diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 0add478..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: CI - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - test: - name: Build and test - runs-on: ubuntu-latest - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y cmake ninja-build catch2 - - - name: Configure - run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DENABLE_TEST=ON - - - name: Build - run: cmake --build build - - - name: Test - run: ctest --test-dir build --output-on-failure diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 933585e..0000000 --- a/.gitignore +++ /dev/null @@ -1,30 +0,0 @@ -# Build outputs -build/ -build-*/ -out/ -tmp_cmake/ -cmake-build-*/ -CMakeFiles/ -CMakeCache.txt -Testing/ -bin/ -jqcpp - -# Editor and tooling state -.cache/ -.vscode/ -.idea/ -compile_commands.json - -# Local configuration and credentials -.env -.env.* -!.env.example -*.pem -*.key -*.p12 -*.pfx -id_rsa* -id_ed25519* -*secret* -*credential* diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..8c3fd11 --- /dev/null +++ b/.nojekyll @@ -0,0 +1 @@ +# Disable Jekyll processing for GitHub Pages. diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 22c20b6..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,77 +0,0 @@ -cmake_minimum_required(VERSION 3.14.0) -project(jqcpp VERSION 1.0.0 LANGUAGES C CXX) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Generate compile_commands.json -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -#set(CMAKE_VERBOSE_MAKEFILE true) - -include(${CMAKE_SOURCE_DIR}/Sanitizers.cmake) -include_directories(${PROJECT_SOURCE_DIR}/include) - -file(GLOB JQCPP_SOURCES "src/*.cpp") - -option(ENABLE_TEST "enable build the tests" OFF) -if (ENABLE_TEST) - -find_package(Catch2 REQUIRED) - - -# JSON Tokenizer test -add_executable(test_json_tokenizer tests/test_json_tokenizer.cpp src/json_tokenizer.cpp) -target_link_libraries(test_json_tokenizer PRIVATE Catch2::Catch2WithMain) - -# JSON Parser test -add_executable(test_json_parser tests/test_json_parser.cpp src/json_parser.cpp src/json_tokenizer.cpp) -target_link_libraries(test_json_parser PRIVATE Catch2::Catch2WithMain) - -# JSON Parser test -add_executable(test_pretty_printer tests/test_pretty_printer.cpp src/json_parser.cpp src/json_tokenizer.cpp src/pretty_printer.cpp) -target_link_libraries(test_pretty_printer PRIVATE Catch2::Catch2WithMain) - -# expression tokenizer test -add_executable(test_expression_tokenizer tests/test_expression_tokenizer.cpp ${JQCPP_SOURCES}) -target_link_libraries(test_expression_tokenizer PRIVATE Catch2::Catch2WithMain) - -# expression interpreter test -add_executable(test_expression_interpreter tests/test_expression_interpreter.cpp ${JQCPP_SOURCES}) -target_link_libraries(test_expression_interpreter PRIVATE Catch2::Catch2WithMain) - -# jqcpp test -add_executable(test_jqcpp tests/test_jqcpp.cpp ${JQCPP_SOURCES}) -target_link_libraries(test_jqcpp PRIVATE Catch2::Catch2WithMain) - - -# Enable testing -enable_testing() -add_test(NAME json_tokenizer_test COMMAND test_json_tokenizer) -add_test(NAME json_parser_test COMMAND test_json_parser) -add_test(NAME json_pretty_printer COMMAND test_pretty_printer) -add_test(NAME expression_tokenizer_test COMMAND test_expression_tokenizer) -add_test(NAME expression_interpreter_test COMMAND test_expression_interpreter) -add_test(NAME jqcpp_test COMMAND test_jqcpp) - -# Add a custom target to run all tests -add_custom_target(run_tests - COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure - DEPENDS test_json_tokenizer - test_json_parser - test_pretty_printer - test_expression_tokenizer - test_expression_interpreter - test_jqcpp -) - - -endif() # ENABLE_TEST - -# The app -add_executable(jqcpp app/main.cpp ${JQCPP_SOURCES}) - -# Install the command-line executable. -install(TARGETS jqcpp DESTINATION bin) - -# Install the demo script. -install(PROGRAMS demo DESTINATION bin) diff --git a/CMakePresets.json b/CMakePresets.json deleted file mode 100644 index 9b0010a..0000000 --- a/CMakePresets.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "version": 8, - "configurePresets": [ - { - "name": "debug", - "displayName": "debug", - "description": "Using compilers: C = /opt/llvm/bin/clang, CXX = /opt/llvm/bin/clang++", - "binaryDir": "${sourceDir}/out/build/${presetName}", - "cacheVariables": { - "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}", - "CMAKE_C_COMPILER": "clang", - "CMAKE_CXX_COMPILER": "clang++", - "CMAKE_BUILD_TYPE": "Debug", - "ENABLE_ASAN": true, - "ENABLE_UBSAN": true, - "ENABLE_LSAN": true - } - }, - { - "name": "Release", - "displayName": "Release", - "description": "Using compilers: C = /opt/llvm/bin/clang, CXX = /opt/llvm/bin/clang++", - "binaryDir": "${sourceDir}/out/build/${presetName}", - "cacheVariables": { - "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}", - "CMAKE_C_COMPILER": "clang", - "CMAKE_CXX_COMPILER": "clang++", - "CMAKE_BUILD_TYPE": "Release", - "ENABLE_ASAN": true, - "ENABLE_UBSAN": true, - "ENABLE_LSAN": true - } - } - ] -} diff --git a/README.md b/README.md deleted file mode 100644 index 9d5c4ed..0000000 --- a/README.md +++ /dev/null @@ -1,185 +0,0 @@ -# jqcpp - -`jqcpp` is a small C++20 command-line JSON processor inspired by -[`jq`](https://jqlang.github.io/jq/). It parses JSON from standard input or a -file, evaluates a jq-like expression, and writes formatted JSON output. - -The project is intentionally focused on a compact subset of jq syntax. It is -useful as a lightweight JSON parser/evaluator example and as a starting point -for experimenting with query language implementation in modern C++. - -## Features - -- JSON tokenization, parsing, and pretty-printing -- Field access for objects, including nested paths -- Array indexing and slicing -- Identity filters -- Numeric addition and subtraction -- Pipe expressions -- Basic `length` and `keys` functions - -## Requirements - -- CMake 3.14 or newer -- A C++20 compiler -- Catch2, only when building tests with `ENABLE_TEST=ON` - -## Build - -Configure and build the command-line executable: - -```sh -cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -cmake --build build -``` - -Install to a custom prefix: - -```sh -cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/path/to/install -cmake --build build --target install -``` - -The installed binary is available at: - -```sh -/path/to/install/bin/jqcpp -``` - -## Usage - -```sh -jqcpp [options] [input-file] -``` - -Read JSON from standard input: - -```sh -echo '{"name": "John", "age": 30}' | jqcpp '.name' -``` - -Read JSON from a file: - -```sh -jqcpp '.users[0].name' input.json -``` - -Run interactively by providing an expression and then typing or pasting JSON. -Press `Ctrl+D` to end input: - -```sh -jqcpp '.' -``` - -## Examples - -Identity filter: - -```sh -echo '{"name": "John"}' | jqcpp '.' -``` - -Object field access: - -```sh -echo '{"name": "John", "age": 30}' | jqcpp '.name' -``` - -Nested object access: - -```sh -echo '{"person": {"name": "John"}}' | jqcpp '.person.name' -``` - -Array index: - -```sh -echo '[10, 20, 30]' | jqcpp '.[1]' -``` - -Array slice: - -```sh -echo '[10, 20, 30, 40]' | jqcpp '.[1:3]' -``` - -Arithmetic: - -```sh -echo '{"x": 10, "y": 5}' | jqcpp '.x + .y' -``` - -Pipe expressions: - -```sh -echo '{"users": [{"name": "Alice"}, {"name": "Bob"}]}' | jqcpp '.users | .[0].name' -``` - -Built-in functions: - -```sh -echo '{"a": 1, "b": 2}' | jqcpp 'keys' -echo '[1, 2, 3]' | jqcpp 'length' -``` - -## Project Video - -Watch a short introduction to `jqcpp`: - -## Supported Expression Syntax - -| Expression | Description | -| --- | --- | -| `.` | Return the input unchanged | -| `.field` | Read an object field | -| `.field.nested` | Read nested object fields | -| `.[index]` | Read an array element by zero-based index | -| `.[start:end]` | Return an array slice | -| `.[start:]` | Return an array slice from `start` to the end | -| `.[:end]` | Return an array slice from the beginning to `end` | -| `.[]` | Iterate object values or array elements into an array result | -| ` + ` | Add numeric results | -| ` - ` | Subtract numeric results | -| ` | ` | Pipe one expression result into the next expression | -| `length` | Return the length of an array, string, or object | -| `keys` | Return object keys or array indexes | - -## Tests - -Build and run tests with Catch2 available to CMake: - -```sh -cmake -S . -B build-tests -DENABLE_TEST=ON -cmake --build build-tests -ctest --test-dir build-tests --output-on-failure -``` - -The CMake project also defines a convenience target: - -```sh -cmake --build build-tests --target run_tests -``` - -## Project Layout - -```text -app/ Command-line entry point -include/jqcpp/ Public headers -src/ Parser, evaluator, tokenizer, and printer implementation -tests/ Catch2 test suite -demo Installed demo script -``` - -## Limitations - -`jqcpp` implements a jq-like subset rather than full jq compatibility. Advanced -jq features such as filters with multiple independent outputs, variables, -conditionals, object construction, full function definitions, and regular -expressions are not currently implemented. - -## Open-Source Publishing Notes - -Before publishing, add a `LICENSE` file that matches the terms you want for -external reuse. If you publish the existing git history, rewrite or replace the -history first because earlier commits contain personal course-identification -metadata and generated build artifacts. diff --git a/Sanitizers.cmake b/Sanitizers.cmake deleted file mode 100644 index e625ce1..0000000 --- a/Sanitizers.cmake +++ /dev/null @@ -1,23 +0,0 @@ -option(ENABLE_ASAN "Enable Address Sanitizer" OFF) -option(ENABLE_UBSAN "Enable Undefined Behavior Sanitizer" OFF) -option(ENABLE_LSAN "Enable Leak Sanitizer" OFF) - - -if (ENABLE_ASAN) - message(STATUS "Address Sanitizer enabled") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address") -endif() - -if (ENABLE_UBSAN) - message(STATUS "Undefined Behavior Sanitizer enabled") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=undefined") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined") -endif() - -if (ENABLE_LSAN) - message(STATUS "Leak Sanitizer enabled") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=leak") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=leak") -endif() - diff --git a/app/expression_repl.cpp b/app/expression_repl.cpp deleted file mode 100644 index 7072f01..0000000 --- a/app/expression_repl.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include -#include - -std::string read_json_input(std::istream &input) { - std::string line; - if (std::getline(input, line)) { - return line; - } - return "q"; -} - -int main(int argc, char *argv[]) { - while (true) { - std::string filter = read_json_input(std::cin); - if (filter == "q") { - break; - } - jqcpp::JQInterpreter interpreter(filter); - // std::cout << interpreter.prettyPrint() << "\n"; - }; - return 0; -} diff --git a/app/main.cpp b/app/main.cpp deleted file mode 100644 index 863eb32..0000000 --- a/app/main.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "jqcpp/jq_interpreter.hpp" -#include - -int main(int argc, char *argv[]) { - return jqcpp::run_jqcpp(argc, argv, std::cin, std::cout); -} diff --git a/demo b/demo deleted file mode 100755 index 9db486f..0000000 --- a/demo +++ /dev/null @@ -1,24 +0,0 @@ -#! /usr/bin/env bash - -# Print an error message and exit. -panic() -{ - echo "ERROR: $@" - exit 1 -} - -# Get the directory in which the currently running script is located. -cmd_dir=$(dirname "$0") || panic "cannot determine command directory" - -jqcpp="$cmd_dir/jqcpp" - -echo "Running jqcpp help" -$jqcpp --help || panic "hello program failed" - -echo "" - - -echo "=======================================================" -echo "For example, you can run jqcpp like this: " -echo "echo '{\"name\": \"John\", \"age\": 35}' | $jqcpp '.age + 5'" -echo '{"name": "John", "age": 35}' | $jqcpp '.age + 5' diff --git a/include/jqcpp/jq_ast_node.hpp b/include/jqcpp/jq_ast_node.hpp deleted file mode 100644 index bf9d24d..0000000 --- a/include/jqcpp/jq_ast_node.hpp +++ /dev/null @@ -1,170 +0,0 @@ -// jq_ast_node.hpp -#pragma once -#include "jq_ast_visitor.hpp" -#include -#include - -namespace jqcpp { - -enum class ASTNodeType { - Identity, - Field, - ArrayIndex, - ArraySlice, - ObjectAccess, - ObjectIterator, - Addition, - Subtraction, - Length, - Keys, - Pipe, - Literal, - NumberLiteralNode, -}; - -class ASTNode { -public: - ASTNodeType type; - std::string value; - std::unique_ptr left; - std::unique_ptr right; - - ASTNode(ASTNodeType t, std::string v = "", - std::unique_ptr l = nullptr, - std::unique_ptr r = nullptr) - : type(t), value(std::move(v)), left(std::move(l)), right(std::move(r)) {} - - virtual ~ASTNode() = default; - - virtual json::JSONValue accept(ASTVisitor &visitor) const = 0; -}; - -class IdentityNode : public ASTNode { -public: - IdentityNode() : ASTNode(ASTNodeType::Identity) {} - json::JSONValue accept(ASTVisitor &visitor) const override { - return visitor.visitIdentity(*this); - } -}; - -class FieldNode : public ASTNode { -public: - FieldNode(std::string fieldName) - : ASTNode(ASTNodeType::Field, std::move(fieldName)) {} - json::JSONValue accept(ASTVisitor &visitor) const override { - return visitor.visitField(*this); - } -}; - -class ArrayIndexNode : public ASTNode { -public: - ArrayIndexNode(std::unique_ptr array, std::unique_ptr index) - : ASTNode(ASTNodeType::ArrayIndex, "", std::move(array), - std::move(index)) {} - json::JSONValue accept(ASTVisitor &visitor) const override { - return visitor.visitArrayIndex(*this); - } -}; - -class ArraySliceNode : public ASTNode { -public: - ArraySliceNode(std::unique_ptr array, std::unique_ptr start, - std::unique_ptr end) - : ASTNode(ASTNodeType::ArraySlice), array(std::move(array)), - start(std::move(start)), end(std::move(end)) {} - - json::JSONValue accept(ASTVisitor &visitor) const override { - return visitor.visitArraySlice(*this); - } - - std::unique_ptr array; - std::unique_ptr start; - std::unique_ptr end; -}; - -class ObjectAccessNode : public ASTNode { -public: - ObjectAccessNode(std::unique_ptr object, - std::unique_ptr field) - : ASTNode(ASTNodeType::ObjectAccess, "", std::move(object), - std::move(field)) {} - json::JSONValue accept(ASTVisitor &visitor) const override { - return visitor.visitObjectAccess(*this); - } -}; - -class ObjectIteratorNode : public ASTNode { -public: - ObjectIteratorNode(std::unique_ptr object) - : ASTNode(ASTNodeType::ObjectIterator, "", std::move(object)) {} - json::JSONValue accept(ASTVisitor &visitor) const override { - return visitor.visitObjectIterator(*this); - } -}; - -class AdditionNode : public ASTNode { -public: - AdditionNode(std::unique_ptr left, std::unique_ptr right) - : ASTNode(ASTNodeType::Addition, "", std::move(left), std::move(right)) {} - json::JSONValue accept(ASTVisitor &visitor) const override { - return visitor.visitAddition(*this); - } -}; - -class SubtractionNode : public ASTNode { -public: - SubtractionNode(std::unique_ptr left, std::unique_ptr right) - : ASTNode(ASTNodeType::Subtraction, "", std::move(left), - std::move(right)) {} - json::JSONValue accept(ASTVisitor &visitor) const override { - return visitor.visitSubtraction(*this); - } -}; - -class LengthNode : public ASTNode { -public: - LengthNode() : ASTNode(ASTNodeType::Length) {} - json::JSONValue accept(ASTVisitor &visitor) const override { - return visitor.visitLength(*this); - } -}; - -class KeysNode : public ASTNode { -public: - KeysNode() : ASTNode(ASTNodeType::Keys) {} - json::JSONValue accept(ASTVisitor &visitor) const override { - return visitor.visitKeys(*this); - } -}; - -class PipeNode : public ASTNode { -public: - PipeNode(std::unique_ptr left, std::unique_ptr right) - : ASTNode(ASTNodeType::Pipe, "", std::move(left), std::move(right)) {} - json::JSONValue accept(ASTVisitor &visitor) const override { - return visitor.visitPipe(*this); - } -}; - -class LiteralNode : public ASTNode { -public: - LiteralNode(double value) - : ASTNode(ASTNodeType::Literal, std::to_string(value)) {} - json::JSONValue accept(ASTVisitor &visitor) const override { - return visitor.visitLiteral(*this); - } -}; - -class NumberLiteralNode : public ASTNode { - -public: - NumberLiteralNode(double value) - : ASTNode(ASTNodeType::NumberLiteralNode), value(value) {} - json::JSONValue accept(ASTVisitor &visitor) const override { - return visitor.visitNumberLiteral(*this); - } - - double value; -}; - -} // namespace jqcpp diff --git a/include/jqcpp/jq_ast_visitor.hpp b/include/jqcpp/jq_ast_visitor.hpp deleted file mode 100644 index 52976d1..0000000 --- a/include/jqcpp/jq_ast_visitor.hpp +++ /dev/null @@ -1,41 +0,0 @@ -// jq_ast_visitor.hpp -#pragma once -#include "json_value.hpp" - -namespace jqcpp { - -class ASTNode; -class IdentityNode; -class FieldNode; -class ArrayIndexNode; -class ArraySliceNode; -class ObjectAccessNode; -class ObjectIteratorNode; -class AdditionNode; -class SubtractionNode; -class LengthNode; -class KeysNode; -class PipeNode; -class LiteralNode; -class NumberLiteralNode; - -class ASTVisitor { -public: - virtual ~ASTVisitor() = default; - virtual json::JSONValue visitIdentity(const IdentityNode &node) = 0; - virtual json::JSONValue visitField(const FieldNode &node) = 0; - virtual json::JSONValue visitArrayIndex(const ArrayIndexNode &node) = 0; - virtual json::JSONValue visitArraySlice(const ArraySliceNode &node) = 0; - virtual json::JSONValue visitObjectAccess(const ObjectAccessNode &node) = 0; - virtual json::JSONValue - visitObjectIterator(const ObjectIteratorNode &node) = 0; - virtual json::JSONValue visitAddition(const AdditionNode &node) = 0; - virtual json::JSONValue visitSubtraction(const SubtractionNode &node) = 0; - virtual json::JSONValue visitLength(const LengthNode &node) = 0; - virtual json::JSONValue visitKeys(const KeysNode &node) = 0; - virtual json::JSONValue visitPipe(const PipeNode &node) = 0; - virtual json::JSONValue visitLiteral(const LiteralNode &node) = 0; - virtual json::JSONValue visitNumberLiteral(const NumberLiteralNode &node) = 0; -}; - -} // namespace jqcpp diff --git a/include/jqcpp/jq_evaluator.hpp b/include/jqcpp/jq_evaluator.hpp deleted file mode 100644 index 761a2ad..0000000 --- a/include/jqcpp/jq_evaluator.hpp +++ /dev/null @@ -1,40 +0,0 @@ -// jq_evaluator.hpp -#pragma once - -#include "jqcpp/jq_ast_visitor.hpp" -#include "jqcpp/json_value.hpp" -#include - -namespace jqcpp { - -class JQEvaluator : public ASTVisitor { -public: - json::JSONValue evaluate(const ASTNode &node, const json::JSONValue &input); - - json::JSONValue visitIdentity(const IdentityNode &node) override; - json::JSONValue visitField(const FieldNode &node) override; - json::JSONValue visitArrayIndex(const ArrayIndexNode &node) override; - json::JSONValue visitArraySlice(const ArraySliceNode &node) override; - json::JSONValue visitObjectAccess(const ObjectAccessNode &node) override; - json::JSONValue visitObjectIterator(const ObjectIteratorNode &node) override; - json::JSONValue visitAddition(const AdditionNode &node) override; - json::JSONValue visitSubtraction(const SubtractionNode &node) override; - json::JSONValue visitLength(const LengthNode &node) override; - json::JSONValue visitKeys(const KeysNode &node) override; - json::JSONValue visitPipe(const PipeNode &node) override; - json::JSONValue visitLiteral(const LiteralNode &node) override; - json::JSONValue visitNumberLiteral(const NumberLiteralNode &node) override; - -private: - std::stack contextStack; - - const json::JSONValue ¤tContext() const { return *contextStack.top(); } - - void pushContext(const json::JSONValue &context) { - contextStack.push(&context); - } - - void popContext() { contextStack.pop(); } -}; - -} // namespace jqcpp diff --git a/include/jqcpp/jq_interpreter.hpp b/include/jqcpp/jq_interpreter.hpp deleted file mode 100644 index 5e0bb8d..0000000 --- a/include/jqcpp/jq_interpreter.hpp +++ /dev/null @@ -1,29 +0,0 @@ -// jq_interpreter.hpp -#pragma once -#include "jq_evaluator.hpp" -#include "jq_parser.hpp" -#include "json_value.hpp" -#include - -namespace jqcpp { - -int run_jqcpp(int argc, char *argv[], std::istream &input, - std::ostream &output); - -class JQInterpreter { - std::string expr_; - -public: - JQInterpreter(const std::string &expr) : expr_(expr) {} - json::JSONValue execute(const std::string &jqExpression, - const json::JSONValue &input); - json::JSONValue execute(const json::JSONValue &input) { - return execute(expr_, input); - } - -private: - JQParser parser; - JQEvaluator evaluator; -}; - -} // namespace jqcpp diff --git a/include/jqcpp/jq_lex.hpp b/include/jqcpp/jq_lex.hpp deleted file mode 100644 index 77dea47..0000000 --- a/include/jqcpp/jq_lex.hpp +++ /dev/null @@ -1,239 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include - -namespace jqcpp { - -class TokenizerError : public std::runtime_error { -public: - TokenizerError(const std::string &message) : std::runtime_error(message) {} -}; - -enum class TokenType { - Dot, - LeftBracket, - RightBracket, - Identifier, - Number, - Pipe, - Comma, - LeftParen, - RightParen, - Semicolon, - QuestionMark, - At, - Dollar, - String, - Equals, - Plus, - Minus, - Multiply, - Divide, - Modulo, - EqualEqual, - NotEqual, - Less, - LessEqual, - Greater, - GreaterEqual, - And, - Or, - Not, - Colon, - LeftBrace, - RightBrace, - Length, - Keys, - End -}; - -struct Token { - TokenType type; - std::string value; - Token(TokenType t, std::string v = "") : type(t), value(std::move(v)) {} -}; - -class JQLexer { -public: - std::vector tokenize(const std::string &expr) { - std::vector tokens; - it = expr.begin(); - end = expr.end(); - - while (it != end) { - tokens.push_back(next_token()); - } - - tokens.push_back(Token(TokenType::End)); - return tokens; - } - -private: - std::string::const_iterator it; - std::string::const_iterator end; - - char peek() const { return (it != end) ? *it : '\0'; } - char peek_next() const { return (it + 1 != end) ? *(it + 1) : '\0'; } - char get() { return (it != end) ? *it++ : '\0'; } - - Token process_identifier_state() { - std::string value; - while (it != end && is_identifier_part(peek())) { - value += get(); - } - // Check for keywords - static const std::unordered_map keywords = { - {"and", TokenType::And}, - {"or", TokenType::Or}, - {"not", TokenType::Not}, - {"length", TokenType::Length}, - {"keys", TokenType::Keys}, - {"null", TokenType::Identifier}, // Treat null as a special identifier - {"true", TokenType::Identifier}, // Treat true as a special identifier - {"false", TokenType::Identifier} // Treat false as a special identifier - }; - - auto it = keywords.find(value); - if (it != keywords.end()) { - return Token(it->second, value); - } - return Token(TokenType::Identifier, value); - } - - Token process_number_state() { - std::string value; - bool has_dot = false; - bool has_e = false; - - while (it != end) { - if (std::isdigit(peek())) { - value += get(); - } else if (peek() == '.' && !has_dot && !has_e) { - has_dot = true; - value += get(); - } else if ((peek() == 'e' || peek() == 'E') && !has_e) { - has_e = true; - value += get(); - if (it != end && (peek() == '+' || peek() == '-')) { - value += get(); - } - } else { - break; - } - } - return Token(TokenType::Number, value); - } - - Token process_string_state(char quote) { - std::string value; - get(); // skip the quote - while (it != end && peek() != quote) { - // escape - if (peek() == '\\' && it + 1 != end) { - get(); - switch (peek()) { - case 'n': - value += '\n'; - break; - case 'r': - value += '\r'; - break; - case 't': - value += '\t'; - break; - case 'b': - value += '\b'; - break; - case 'f': - value += '\f'; - break; - default: - value += peek(); - } - } else { - value += peek(); - } - get(); - } - if (it == end) { - throw TokenizerError("Unterminated string"); - } - get(); // skip ending quote - return Token(TokenType::String, value); - } - Token process_operator_state() { - static const std::unordered_map operators = { - {".", TokenType::Dot}, - {"+", TokenType::Plus}, - {"-", TokenType::Minus}, - {"[", TokenType::LeftBracket}, - {"]", TokenType::RightBracket}, - {":", TokenType::Colon} - - // TODO: the following operations are not supported currently - // {"|", TokenType::Pipe}, - // {",", TokenType::Comma}, {"=", TokenType::Equals}, - // {";", TokenType::Semicolon}, {"?", TokenType::QuestionMark}, - // {"@", TokenType::At}, {"$", TokenType::Dollar}, - // {"*", TokenType::Multiply}, {"/", TokenType::Divide}, - // {"%", TokenType::Modulo}, , - // {"==", TokenType::EqualEqual}, {"!=", TokenType::NotEqual}, - // {"<", TokenType::Less}, {"<=", TokenType::LessEqual}, - // {">", TokenType::Greater}, {">=", TokenType::GreaterEqual} - - }; - std::string op; - op += get(); - if (it != end) { - // determine whether is a valid two-characters op - std::string potential_op = op + peek(); - if (operators.find(potential_op) != operators.end()) { - op = potential_op; - get(); - } - } - auto op_it = operators.find(op); - if (op_it != operators.end()) { - return Token(op_it->second, op); - } - throw TokenizerError("Tokenize Error: Unknow operator: " + op); - } - - bool is_identifier_start(char c) const { - return std::isalpha(c) || c == '_' || c == '$'; - } - - bool is_identifier_part(char c) const { - return std::isalnum(c) || c == '_' || c == '$'; - } - - Token next_token() { - skip_whitespace(); - if (it == end) { - return Token(TokenType::End); - } - - char c = peek(); - if (is_identifier_start(c)) { - return process_identifier_state(); - } - if (std::isdigit(c)) { - return process_number_state(); - } - if (c == '"' || c == '\'') { - return process_string_state(c); - } - return process_operator_state(); - } - - void skip_whitespace() { - while (it != end && std::isspace(peek())) { - get(); - } - } -}; - -} // namespace jqcpp diff --git a/include/jqcpp/jq_parser.hpp b/include/jqcpp/jq_parser.hpp deleted file mode 100644 index f362637..0000000 --- a/include/jqcpp/jq_parser.hpp +++ /dev/null @@ -1,38 +0,0 @@ -// jq_parser.hpp -#pragma once -#include "jq_ast_node.hpp" -#include "jq_lex.hpp" -#include -#include - -namespace jqcpp { - -class JQParser { -public: - std::unique_ptr parse(const std::vector &tokens); - -private: - std::vector::const_iterator current; - std::vector::const_iterator end; - - std::unique_ptr parseExpression(); - std::unique_ptr parseTerm(); - std::unique_ptr parseFieldAccess(std::unique_ptr base); - std::unique_ptr parseArrayAccess(std::unique_ptr base); - std::unique_ptr parseSlice(std::unique_ptr base, - std::unique_ptr start); - std::unique_ptr parseObjectAccess(std::unique_ptr base); - std::unique_ptr parseObjectIterator(std::unique_ptr base); - std::unique_ptr parseAddition(); - std::unique_ptr parseSubtraction(); - std::unique_ptr parseLength(); - std::unique_ptr parseKeys(); - - Token peek() const; - Token advance(); - bool match(TokenType type); - void consume(TokenType type, const std::string &message); - bool isAtEnd() const; -}; - -} // namespace jqcpp diff --git a/include/jqcpp/json_parser.hpp b/include/jqcpp/json_parser.hpp deleted file mode 100644 index f250c28..0000000 --- a/include/jqcpp/json_parser.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once -#include "json_tokenizer.hpp" -#include "json_value.hpp" -#include -#include -#include - -namespace jqcpp::json { -class JSONParser { -public: - JSONValue parse(const std::vector &tokens); - -private: - // parse methods - JSONValue parse_value(); - JSONValue parse_object(); - JSONValue parse_array(); - void consume(TokenType expected_type); - - // iterators - std::vector::const_iterator it; - std::vector::const_iterator end; -}; - -class JSONParserError : public std::runtime_error { -public: - JSONParserError(const std::string &message) : std::runtime_error(message) {} -}; - -} // namespace jqcpp::json diff --git a/include/jqcpp/json_tokenizer.hpp b/include/jqcpp/json_tokenizer.hpp deleted file mode 100644 index fa59db2..0000000 --- a/include/jqcpp/json_tokenizer.hpp +++ /dev/null @@ -1,56 +0,0 @@ -#pragma once -#include -#include -#include -namespace jqcpp::json { - -enum class TokenType { - LeftBrace, - RightBrace, - LeftBracket, - RightBracket, - Comma, - Colon, - String, - Number, - True, - False, - Null, - EndOfInput -}; - -struct Token { - TokenType type; - std::string value; - Token(TokenType t, const std::string &v = "") : type(t), value(v) {} -}; - -class JSONTokenizer { -public: - std::vector tokenize(const std::string &json_string); - -private: - std::string::const_iterator it; - std::string::const_iterator end; - - Token next_token(); - Token parse_string(); - Token parse_number(); - Token parse_true(); - Token parse_false(); - Token parse_null(); - void skip_whitespace(); - char peek() const { return (it != end) ? *it : '\0'; } - char get() { return (it != end) ? *it++ : '\0'; } - bool is_digit(char c) const { return c >= '0' && c <= '9'; } - bool is_hex_digit(char c) const { - return is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); - } -}; - -class JSONTokenizerError : public std::runtime_error { -public: - JSONTokenizerError(const std::string &message) - : std::runtime_error(message) {} -}; -} // namespace jqcpp::json diff --git a/include/jqcpp/json_value.hpp b/include/jqcpp/json_value.hpp deleted file mode 100644 index 59b1f42..0000000 --- a/include/jqcpp/json_value.hpp +++ /dev/null @@ -1,210 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include - -namespace jqcpp::json { -struct JSONValue; -using JSONArray = std::vector; -// use vector instead of map for maintain the insertion order -// however, need be careful when insert new pair -// for the exist keys just update the values -using JSONObject = std::vector>; - -inline void jsonObjectInsert(JSONObject &obj, const std::string &key, - JSONValue v); - -struct JSONValue { - std::variant, std::unique_ptr> - value; - - // constructors - JSONValue() : value(nullptr) {} - JSONValue(std::string v) : value(std::move(v)) {} - JSONValue(double v) : value(v) {} - JSONValue(bool v) : value(v) {} - JSONValue(std::nullptr_t v) : value(nullptr) {} - JSONValue(JSONArray v) : value(std::make_unique(std::move(v))) {} - JSONValue(JSONObject v) : value(std::make_unique(std::move(v))) {} - - // copy - JSONValue(const JSONValue &other) = delete; - JSONValue &operator=(const JSONValue &other) = delete; - - // move - JSONValue(JSONValue &&other) noexcept = default; - JSONValue &operator=(JSONValue &&) = default; - - // helper functions for type checker - // these functions are helpful in testing - bool is_bool() const { return std::holds_alternative(value); } - // all the numbers are converted to double - bool is_number() const { return std::holds_alternative(value); } - bool is_string() const { return std::holds_alternative(value); } - // null - bool is_null() const { return std::holds_alternative(value); } - // object - bool is_object() const { - return std::holds_alternative>(value); - } - - bool is_array() const { - return std::holds_alternative>(value); - } - - // array - // getters - bool get_bool() const { return std::get(value); } - double get_number() const { return std::get(value); } - const std::string &get_string() const { return std::get(value); } - const JSONObject &get_object() const { - if (!is_object()) { - throw std::runtime_error("Not a JSONObject"); - } - return *std::get>(value); - } - - const JSONArray &get_array() const { - if (!is_array()) { - throw std::runtime_error("Not a JSON Array"); - } - return *std::get>(value); - } - - JSONValue deepCopy() const { - if (is_string()) { - return JSONValue(get_string()); - } else if (is_number()) { - return JSONValue(get_number()); - } else if (is_bool()) { - return JSONValue(get_bool()); - } else if (is_null()) { - return JSONValue(); - } else if (is_array()) { - JSONArray new_array; - for (const auto &elem : get_array()) { - new_array.push_back(elem.deepCopy()); - } - return JSONValue(std::move(new_array)); - } else if (is_object()) { - JSONObject new_object; - for (const auto &[key, value] : get_object()) { - jsonObjectInsert(new_object, key, value.deepCopy()); - } - return JSONValue(std::move(new_object)); - } - throw std::runtime_error("Unknown JSONValue type in deepCopy"); - } - - // override operations - const JSONValue &operator[](std::size_t index) const; - const JSONValue &operator[](const std::string &index) const; -}; - -inline void jsonObjectInsert(JSONObject &obj, const std::string &key, - JSONValue v) { - auto it = std::find_if(obj.begin(), obj.end(), [&key](const auto &pair) { - return pair.first == key; - }); - if (it != obj.end()) { - // found, update the value - it->second = std::move(v); - } else { - obj.emplace_back(key, std::move(v)); - } -} - -// Addition Operator -inline JSONValue operator+(const JSONValue &lhs, const JSONValue &rhs) { - if (lhs.is_number() && rhs.is_number()) { - return JSONValue(lhs.get_number() + rhs.get_number()); - } - if (lhs.is_string() && rhs.is_string()) { - return JSONValue(lhs.get_string() + rhs.get_string()); - } - throw std::runtime_error("Invalid types for addition"); -} - -// Subtraction Operator -inline JSONValue operator-(const JSONValue &lhs, const JSONValue &rhs) { - if (lhs.is_number() && rhs.is_number()) { - return JSONValue(lhs.get_number() - rhs.get_number()); - } - if (lhs.is_string() && rhs.is_string()) { - const std::string &lhs_str = lhs.get_string(); - const std::string &rhs_str = rhs.get_string(); - size_t pos = lhs_str.find(rhs_str); - if (pos != std::string::npos) { - std::string result = lhs_str; - result.erase(pos, rhs_str.length()); - return JSONValue(result); - } - return JSONValue(lhs_str); // return lhs if rhs is not found - } - throw std::runtime_error("Invalid types for subtraction"); -} - -// Length Function -inline std::size_t length(const JSONValue &json) { - if (json.is_array()) { - return json.get_array().size(); - } - if (json.is_object()) { - return json.get_object().size(); - } - if (json.is_string()) { - return json.get_string().length(); - } - throw std::runtime_error("Invalid type for length"); -} - -// Keys Function -inline JSONArray keys(const JSONValue &json) { - JSONArray result; - if (json.is_object()) { - for (const auto &pair : json.get_object()) { - result.push_back(JSONValue(pair.first)); - } - return result; - } - if (json.is_array()) { - for (std::size_t i = 0; i < json.get_array().size(); ++i) { - result.push_back(JSONValue(static_cast(i))); - } - return result; - } - throw std::runtime_error("Invalid type for keys"); -} - -inline const JSONValue &JSONValue::operator[](std::size_t index) const { - if (!is_array()) { - throw std::runtime_error("Not an array"); - } - const auto &array = get_array(); - if (index >= array.size()) { - throw std::runtime_error("Array index out of bounds"); - } - return array[index]; -} - -// Overload operator[] for object access -inline const JSONValue &JSONValue::operator[](const std::string &key) const { - if (!is_object()) { - throw std::runtime_error("Not an object"); - } - const auto &object = get_object(); - auto it = - std::find_if(object.begin(), object.end(), - [&key](const auto &pair) { return pair.first == key; }); - if (it == object.end()) { - throw std::runtime_error("Object key not found"); - } - return it->second; -} - -} // namespace jqcpp::json diff --git a/include/jqcpp/pretty_printer.hpp b/include/jqcpp/pretty_printer.hpp deleted file mode 100644 index 54d8a9e..0000000 --- a/include/jqcpp/pretty_printer.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once -#include "json_value.hpp" - -#include - -namespace jqcpp::json { - -/** - * @class JSONPrinter - * @brief pretty print a json object - * - */ -class JSONPrinter { -public: - std::string print(const JSONValue &value, int indent = 0); - -private: - std::string indent_string(int indent); - std::string print_object(const JSONObject &obj, int indent); - std::string print_array(const JSONArray &arr, int indent); -}; - -} // namespace jqcpp::json diff --git a/index.html b/index.html new file mode 100644 index 0000000..85d2183 --- /dev/null +++ b/index.html @@ -0,0 +1,303 @@ + + + + + + + jqcpp - C++ JSON Processor + + + +
+
+
jqcpp
+ +
+ +
+
+
+

jqcpp

+

+ A compact C++20 command-line JSON processor inspired by jq, built + around a tokenizer, parser, evaluator, and pretty-printer. +

+ +
+ +
+
jqcpp example
+
echo '{"x": 10, "y": 5}' | jqcpp '.x + .y'
+15
+
+
+ +
+

What it supports

+
+
+

JSON processing

+

Tokenize, parse, evaluate, and pretty-print JSON input.

+
+
+

jq-like filters

+

Use identity filters, field access, array indexes, and slices.

+
+
+

Small expression set

+

Use pipes, numeric arithmetic, length, and keys.

+
+
+
+ +
+

Build from source

+
+
CMake
+
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
+cmake --build build
+
+
+ +
+

Project introduction

+
+

+ Watch the short jqcpp introduction video for an overview of the + project and command-line behavior. +

+ + Open YouTube video + +
+
+
+ +
+ jqcpp is a focused learning-oriented implementation of a jq-like JSON + processor in C++20. +
+
+ + diff --git a/src/jq_evaluator.cpp b/src/jq_evaluator.cpp deleted file mode 100644 index 2531336..0000000 --- a/src/jq_evaluator.cpp +++ /dev/null @@ -1,187 +0,0 @@ -// jq_evaluator.cpp -#include "jqcpp/jq_evaluator.hpp" -#include "jqcpp/jq_ast_node.hpp" - -namespace jqcpp { - -json::JSONValue JQEvaluator::evaluate(const ASTNode &node, - const json::JSONValue &input) { - contextStack.push(&input); - auto result = node.accept(*this); - contextStack.pop(); - return result; -} - -json::JSONValue JQEvaluator::visitIdentity(const IdentityNode &node) { - return currentContext().deepCopy(); -} - -json::JSONValue JQEvaluator::visitField(const FieldNode &node) { - if (!currentContext().is_object()) { - throw std::runtime_error("Cannot access field of non-object value"); - } - return currentContext()[node.value].deepCopy(); -} - -json::JSONValue JQEvaluator::visitArrayIndex(const ArrayIndexNode &node) { - - auto leftResult = node.left->accept(*this); - - if (!leftResult.is_array()) { - throw std::runtime_error("Cannot access index of non-array value"); - } - - const auto &array = leftResult.get_array(); - - // Evaluate the index expression. - auto indexResult = node.right->accept(*this); - - // Array indexes must evaluate to numbers. - if (!indexResult.is_number()) { - throw std::runtime_error("Array index must be a number"); - } - - size_t index = static_cast(indexResult.get_number()); - - if (index >= array.size()) { - return json::JSONValue(nullptr); - } - - return array[index].deepCopy(); -} - -json::JSONValue JQEvaluator::visitArraySlice(const ArraySliceNode &node) { - - auto array_node = node.array->accept(*this); - - if (!array_node.is_array()) { - throw std::runtime_error("Cannot slice non-array value"); - } - - const auto &array = array_node.get_array(); - size_t start = 0; - size_t end = array.size(); - - if (node.start) { - auto startValue = node.start->accept(*this); - start = static_cast(startValue.get_number()); - } - if (node.end) { - auto endValue = node.end->accept(*this); - end = static_cast(endValue.get_number()); - } - - start = std::min(start, array.size()); - end = std::min(end, array.size()); - - std::vector values; - for (size_t i = start; i < end; ++i) { - values.push_back(array[i].deepCopy()); - } - - return json::JSONValue(std::move(values)); -} - -json::JSONValue JQEvaluator::visitObjectAccess(const ObjectAccessNode &node) { - auto result = node.left->accept(*this); - contextStack.push(&result); - auto finalResult = node.right->accept(*this); - contextStack.pop(); - return finalResult; -} - -json::JSONValue -JQEvaluator::visitObjectIterator(const ObjectIteratorNode &node) { - auto leftResult = node.left->accept(*this); - if (!(leftResult.is_object() || leftResult.is_array())) { - throw std::runtime_error( - "Cannot iterate over non-object or non-array value"); - } - - json::JSONArray result; - if (leftResult.is_object()) { - const auto &obj = leftResult.get_object(); - for (const auto &[key, value] : obj) { - result.push_back(value.deepCopy()); - } - } else { - const auto &obj = leftResult.get_array(); - for (const auto &value : obj) { - result.push_back(value.deepCopy()); - } - } - - return json::JSONValue(std::move(result)); -} - -json::JSONValue JQEvaluator::visitAddition(const AdditionNode &node) { - auto left = node.left->accept(*this); - auto right = node.right->accept(*this); - if (left.is_number() && right.is_number()) { - return json::JSONValue(left.get_number() + right.get_number()); - } - throw std::runtime_error("Addition is only supported for numbers"); -} - -json::JSONValue JQEvaluator::visitSubtraction(const SubtractionNode &node) { - auto left = node.left->accept(*this); - auto right = node.right->accept(*this); - if (left.is_number() && right.is_number()) { - return json::JSONValue(left.get_number() - right.get_number()); - } - throw std::runtime_error("Subtraction is only supported for numbers"); -} - -json::JSONValue JQEvaluator::visitLength(const LengthNode &node) { - if (currentContext().is_array()) { - return json::JSONValue( - static_cast(currentContext().get_array().size())); - } else if (currentContext().is_string()) { - return json::JSONValue( - static_cast(currentContext().get_string().length())); - } else if (currentContext().is_object()) { - return json::JSONValue( - static_cast(currentContext().get_object().size())); - } - throw std::runtime_error( - "Length is only supported for arrays, strings, and objects"); -} - -json::JSONValue JQEvaluator::visitKeys(const KeysNode &node) { - if (currentContext().is_array()) { - const auto &arr = currentContext().get_array(); - json::JSONArray keys; - for (std::size_t i = 0; i < arr.size(); ++i) { - keys.push_back(json::JSONValue(static_cast(i))); - } - return json::JSONValue(std::move(keys)); - - } else if (currentContext().is_object()) { - const auto &obj = currentContext().get_object(); - json::JSONArray keys; - for (const auto &[key, value] : obj) { - keys.push_back(json::JSONValue(key)); - } - return json::JSONValue(std::move(keys)); - } else { - throw std::runtime_error("Keys is only supported for objects or arrays"); - } -} - -json::JSONValue JQEvaluator::visitPipe(const PipeNode &node) { - auto leftResult = node.left->accept(*this); - contextStack.push(&leftResult); - auto finalResult = node.right->accept(*this); - contextStack.pop(); - return finalResult; -} - -json::JSONValue JQEvaluator::visitLiteral(const LiteralNode &node) { - return json::JSONValue(std::stod(node.value)); -} - -json::JSONValue JQEvaluator::visitNumberLiteral(const NumberLiteralNode &node) { - return json::JSONValue(std::move(node.value)); -} - -} // namespace jqcpp diff --git a/src/jq_interpreter.cpp b/src/jq_interpreter.cpp deleted file mode 100644 index eff1a52..0000000 --- a/src/jq_interpreter.cpp +++ /dev/null @@ -1,128 +0,0 @@ -// jq_interpreter.cpp -#include "jqcpp/jq_interpreter.hpp" -#include "jqcpp/jq_lex.hpp" -#include "jqcpp/json_parser.hpp" -#include "jqcpp/json_tokenizer.hpp" -#include "jqcpp/pretty_printer.hpp" -#include -#include - -namespace jqcpp { - -json::JSONValue JQInterpreter::execute(const std::string &jqExpression, - const json::JSONValue &input) { - JQLexer lexer; - auto tokens = lexer.tokenize(jqExpression); - auto ast = parser.parse(tokens); - return evaluator.evaluate(*ast, input); -} - -std::string read_json_input(std::istream &input) { - std::string line; - std::string json; - while (std::getline(input, line)) { - json += line + "\n"; - } - return json; -} - -void print_version(std::ostream &output) { output << "jqcpp version 1.0.0\n"; } - -void print_help(std::ostream &output) { - output - << "Usage: jqcpp [options] [input-file]\n" - << "\nOptions:\n" - << " -h, --help Display this help information\n" - << " -v, --version Show version information\n" - << "\nInput Methods:\n" - << " 1. Piping JSON data: echo '{\"key\": \"value\"}' | jqcpp " - "''\n" - << " 2. Reading from file: jqcpp '' input.json\n" - << " 3. Interactive mode: jqcpp '' (then type JSON and " - "press Ctrl+D)\n" - << "\nExpression Syntax:\n" - << " . Identity (returns input unchanged)\n" - << " Example: echo '{\"name\": \"John\"}' | jqcpp '.'\n" - << "\n" - << " .foo Object Identifier (retrieve value by field name)\n" - << " Example: echo '{\"name\": \"John\", \"age\": 30}' " - "| jqcpp '.name'\n" - << "\n" - << " .[] Array Index (retrieve element by index)\n" - << " Example: echo '[10, 20, 30]' | jqcpp '.[1]'\n" - << "\n" - << " .[start:end] Array Slicing (retrieve subset of an array)\n" - << " Example: echo '[10, 20, 30, 40]' | jqcpp " - "'.[1:3]'\n" - << "\n" - << " + - Arithmetic Operations\n" - << " Example: echo '{\"x\": 10, \"y\": 5}' | jqcpp '.x " - "+ .y'\n" - << "\n" - << "Built-in Functions:\n" - << " length Returns the length of a string, array, or object\n" - << " keys Returns an array of an object's keys\n" - << " Example: echo '{\"a\": 1, \"b\": 2}' | jqcpp " - "'keys'\n" - << "\nFor more information and examples, see the project README.\n"; -} - -int run_jqcpp(int argc, char *argv[], std::istream &input, - std::ostream &output) { - if (argc < 2) { - print_help(std::cerr); - return 1; - } - std::string arg1 = argv[1]; - if (arg1 == "-h" || arg1 == "--help") { - print_help(output); - return 0; - } - if (arg1 == "-v" || arg1 == "--version") { - print_version(output); - return 0; - } - - std::string expression = arg1; - std::string input_file; - if (argc > 2) { - input_file = argv[2]; - } - - if (expression.empty()) { - std::cerr << "Error: No expression provided\n"; - print_help(output); - } - - std::string json_input; - if (!input_file.empty()) { - std::ifstream ifs(input_file); - if (!ifs) { - std::cerr << "Error: Cannot open file " << input_file << "\n"; - return 1; - } - json_input = read_json_input(ifs); - } else { - json_input = read_json_input(input); - } - - try { - // parse json object - json::JSONTokenizer lexer; - json::JSONParser parser; - auto jvalue = parser.parse(lexer.tokenize(json_input)); - - JQInterpreter interpreter(expression); - auto result = interpreter.execute(jvalue); - json::JSONPrinter printer; - output << printer.print(result) << std::endl; - return 0; - } catch (const std::exception &e) { - std::cerr << "Error: " << e.what() << std::endl; - // throw std::runtime_error("run jqcpp failed: " + std::string(e.what()) + - // "\n"); - return 1; - } -} - -} // namespace jqcpp diff --git a/src/jq_parser.cpp b/src/jq_parser.cpp deleted file mode 100644 index 2bd0fda..0000000 --- a/src/jq_parser.cpp +++ /dev/null @@ -1,186 +0,0 @@ -#include "jqcpp/jq_parser.hpp" -#include "jqcpp/jq_lex.hpp" -#include - -namespace jqcpp { - -std::unique_ptr JQParser::parse(const std::vector &tokens) { - current = tokens.begin(); - end = tokens.end(); - return parseExpression(); -} - -std::unique_ptr JQParser::parseExpression() { - auto node = parseTerm(); - while (match(TokenType::Pipe)) { - auto right = parseTerm(); - node = std::make_unique(std::move(node), std::move(right)); - } - - while (match(TokenType::Plus) || match(TokenType::Minus)) { - TokenType op = std::prev(current)->type; - auto right = parseTerm(); - if (op == TokenType::Plus) { - node = std::make_unique(std::move(node), std::move(right)); - } else { - node = - std::make_unique(std::move(node), std::move(right)); - } - } - - return node; -} - -std::unique_ptr JQParser::parseTerm() { - if (match(TokenType::Number)) { - return std::make_unique( - std::stod(std::prev(current)->value)); - } else if (match(TokenType::Dot)) { - if (isAtEnd() || peek().type == TokenType::Pipe) { - return std::make_unique(); - } - return parseFieldAccess(std::make_unique()); - } else if (match(TokenType::Length)) { - return parseLength(); - } else if (match(TokenType::Keys)) { - return parseKeys(); - } else if (match(TokenType::Plus) || match(TokenType::Minus)) { - return parseAddition(); - } - throw std::runtime_error("Not supported op"); -} - -std::unique_ptr -JQParser::parseFieldAccess(std::unique_ptr base) { - if (match(TokenType::Identifier)) { - auto field = std::make_unique(std::prev(current)->value); - auto node = - std::make_unique(std::move(base), std::move(field)); - - if (match(TokenType::Dot)) { - return parseFieldAccess(std::move(node)); - } else if (match(TokenType::LeftBracket)) { - return parseArrayAccess(std::move(node)); - } - - return node; - } else if (match(TokenType::LeftBracket)) { - return parseArrayAccess(std::move(base)); - } else if (isAtEnd() || peek().type == TokenType::Pipe) { - // This handles the case of a bare '.' (identity) - return base; - } - - throw std::runtime_error( - "Expected identifier, '[', or end of expression after '.'"); -} - -std::unique_ptr -JQParser::parseArrayAccess(std::unique_ptr base) { - if (match(TokenType::Colon)) { - // Slice from the beginning: [:end]. - return parseSlice(std::move(base), nullptr); - } - - std::unique_ptr start; - if (match(TokenType::Number)) { - start = std::make_unique(std::stod(std::prev(current)->value)); - } - - if (match(TokenType::Colon)) { - return parseSlice(std::move(base), std::move(start)); - } - - if (match(TokenType::RightBracket)) { - if (start) { - // Simple array index. - auto node = - std::make_unique(std::move(base), std::move(start)); - if (match(TokenType::Dot)) { - return parseFieldAccess(std::move(node)); - } else if (match(TokenType::LeftBracket)) { - return parseArrayAccess(std::move(node)); - } - return node; - } else { - // Object or array iterator. - return std::make_unique(std::move(base)); - } - } - - throw std::runtime_error("Expected number, ':', or ']' after '['"); -} - -std::unique_ptr JQParser::parseSlice(std::unique_ptr base, - std::unique_ptr start) { - std::unique_ptr end; - - if (!match(TokenType::RightBracket)) { - if (match(TokenType::Number)) { - end = std::make_unique(std::stod(std::prev(current)->value)); - } - consume(TokenType::RightBracket, "Expected ']' after array slice"); - } - - auto node = std::make_unique( - std::move(base), std::move(start), std::move(end)); - - if (match(TokenType::Dot)) { - return parseFieldAccess(std::move(node)); - } else if (match(TokenType::LeftBracket)) { - return parseArrayAccess(std::move(node)); - } - - return node; -} - -std::unique_ptr JQParser::parseAddition() { - auto node = parseSubtraction(); - while (match(TokenType::Plus)) { - auto right = parseSubtraction(); - node = std::make_unique(std::move(node), std::move(right)); - } - return node; -} - -std::unique_ptr JQParser::parseSubtraction() { - auto node = parseTerm(); - while (match(TokenType::Minus)) { - auto right = parseTerm(); - node = std::make_unique(std::move(node), std::move(right)); - } - return node; -} - -std::unique_ptr JQParser::parseLength() { - return std::make_unique(); -} - -std::unique_ptr JQParser::parseKeys() { - return std::make_unique(); -} - -Token JQParser::peek() const { return *current; } - -Token JQParser::advance() { return *current++; } - -bool JQParser::match(TokenType type) { - if (isAtEnd()) - return false; - if (peek().type != type) - return false; - advance(); - return true; -} - -void JQParser::consume(TokenType type, const std::string &message) { - if (peek().type == type) { - advance(); - return; - } - throw std::runtime_error(message); -} - -bool JQParser::isAtEnd() const { return current->type == TokenType::End; } - -} // namespace jqcpp diff --git a/src/json_parser.cpp b/src/json_parser.cpp deleted file mode 100644 index 249f3f2..0000000 --- a/src/json_parser.cpp +++ /dev/null @@ -1,111 +0,0 @@ -#include "jqcpp/json_parser.hpp" -#include "jqcpp/json_tokenizer.hpp" -#include "jqcpp/json_value.hpp" - -namespace jqcpp::json { -JSONValue JSONParser::parse(const std::vector &tokens) { - if (tokens.empty()) { - throw JSONParserError("Empty tokens"); - } - - it = tokens.begin(); - end = tokens.end(); - return parse_value(); -} - -JSONValue JSONParser::parse_value() { - if (it == end) { - throw JSONParserError("Unexpected end of tokens"); - } - - // get a new token - switch (it->type) { - case TokenType::Null: - consume(TokenType::Null); - return JSONValue(nullptr); - case TokenType::True: - consume(TokenType::True); - return JSONValue(true); - case TokenType::False: - consume(TokenType::False); - return JSONValue(false); - case TokenType::Number: { - double v = std::stod(it->value); - consume(TokenType::Number); - return JSONValue(v); - } - case TokenType::String: { - std::string v = it->value; - consume(TokenType::String); - return JSONValue(std::move(v)); - } - case TokenType::LeftBrace: - return parse_object(); - case TokenType::LeftBracket: - return parse_array(); - default: - throw JSONParserError("Unrecognized token type"); - } -} - -void JSONParser::consume(TokenType expected_type) { - if (it == end) { - throw JSONParserError("Unexpected end of token!"); - } - if (it->type != expected_type) { - throw JSONParserError("Unexpected token type"); - } - ++it; -} - -JSONValue JSONParser::parse_object() { - // object is a map which has a key and a value - // a object must start with "{" - consume(TokenType::LeftBrace); - JSONObject object; - - // a right brace is the end of the object - if (it != end && it->type != TokenType::RightBrace) { - do { - // key: value - // key is a string - // value can be any other valid type - // firstly parse the key - if (it->type != TokenType::String) { - throw JSONParserError("The key of object should be a string type"); - } - std::string key = std::move(it->value); - consume(TokenType::String); - // should be colon : - consume(TokenType::Colon); - // the value - jsonObjectInsert(object, key, parse_value()); - // the key: value ends when encounter a comma: , - // and then another object starts - } while (it != end && it->type == TokenType::Comma && (++it, true)); - } - // end of object - consume(TokenType::RightBrace); - // just move - return JSONValue(std::move(object)); -} - -// parse a array -// e.g. [1, 2, 3, 4] -JSONValue JSONParser::parse_array() { - // begin with [ - consume(TokenType::LeftBracket); - // array is a vector of json value - JSONArray arr; - // the array ends if ] appears - if (it != end && it->type != TokenType::RightBracket) { - do { - arr.push_back(parse_value()); - } while (it != end && it->type == TokenType::Comma && (++it, true)); - } - // ends with ] - consume(TokenType::RightBracket); - return JSONValue(std::move(arr)); -} - -} // namespace jqcpp::json diff --git a/src/json_tokenizer.cpp b/src/json_tokenizer.cpp deleted file mode 100644 index 8eeb2f0..0000000 --- a/src/json_tokenizer.cpp +++ /dev/null @@ -1,220 +0,0 @@ -#include "jqcpp/json_tokenizer.hpp" -#include - -namespace jqcpp::json { -std::vector JSONTokenizer::tokenize(const std::string &json_string) { - std::vector tokens; - // initialize the iterators - it = json_string.begin(); - end = json_string.end(); - - // begin parsing - while (it != end) { - auto token = next_token(); - // the end of input - if (token.type == TokenType::EndOfInput) { - break; - } - tokens.push_back(token); - } - return tokens; -} - -Token JSONTokenizer::next_token() { - skip_whitespace(); - if (it == end) { - // end of input - return Token(TokenType::EndOfInput, ""); - } - char c = peek(); - switch (c) { - case '{': - get(); - return Token(TokenType::LeftBrace, "{"); - case '}': - get(); - return Token(TokenType::RightBrace, "}"); - case '[': - get(); - return Token(TokenType::LeftBracket, "["); - case ']': - get(); - return Token(TokenType::RightBracket, "]"); - case ',': - get(); - return Token(TokenType::Comma, ","); - case ':': - get(); - return Token(TokenType::Colon, ":"); - case '"': - return parse_string(); - case '-': - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - return parse_number(); - case 't': - return parse_true(); - case 'f': - return parse_false(); - case 'n': - return parse_null(); - default: - throw JSONTokenizerError("Invalid input"); - } -} - -void JSONTokenizer::skip_whitespace() { - while ((it != end) && std::isspace(*it)) { - ++it; - } -} - -Token JSONTokenizer::parse_null() { - std::string value; - std::string_view expected = "null"; - for (char c : expected) { - if (get() != c) { - throw JSONTokenizerError("Invalid token: expected 'null'"); - } - value += c; - } - return Token(TokenType::Null, value); -} - -Token JSONTokenizer::parse_false() { - std::string value; - std::string_view expected = "false"; - for (char c : expected) { - if (get() != c) { - throw JSONTokenizerError("Invalid token: expected 'false'"); - } - value += c; - } - return Token(TokenType::False, value); -} - -Token JSONTokenizer::parse_true() { - std::string value; - std::string_view expected = "true"; - for (char c : expected) { - if (get() != c) { - throw JSONTokenizerError("Invalid token: expected 'true'"); - } - value += c; - } - return Token(TokenType::True, value); -} - -Token JSONTokenizer::parse_number() { - std::string value; - if (peek() == '-') { - value += get(); - } - if (peek() == '0') { - value += get(); - } else if (is_digit(peek())) { - while (it != end && is_digit(peek())) { - value += get(); - } - } else { - throw JSONTokenizerError("Invalid number format"); - } - - // fraction part - if (peek() == '.') { - value += get(); - // at least on digit - if (!is_digit(peek())) { - throw JSONTokenizerError( - "Invalid number format: digit expected after dot"); - } - while (it != end && is_digit(peek())) { - value += get(); - } - } - - // exponent part - if (peek() == 'e' || peek() == 'E') { - value += get(); - if (peek() == '+' || peek() == '-') { - value += get(); - } - if (!is_digit(peek())) { - throw JSONTokenizerError("Invalid number format: digit expected"); - } - while (it != end && is_digit(peek())) { - value += get(); - } - } - return Token(TokenType::Number, value); -} - -Token JSONTokenizer::parse_string() { - std::string value; - // skip leading " - get(); - while (it != end) { - char c = get(); - if (c == '"') { - // end of string - return Token(TokenType::String, value); - } else if (c == '\\') { - if (it == end) { - throw JSONTokenizerError("Unexpected termination"); - } - char esc = get(); - switch (esc) { - case '"': - case '\\': - case '/': - value += esc; - break; - case 'b': - value += '\b'; - break; - case 'f': - value += '\f'; - break; - case 'n': - value += '\n'; - break; - case 'r': - value += '\r'; - break; - case 't': - value += '\t'; - break; - case 'u': { - - // 4 hex digits - std::string hex; - for (int i = 0; i < 4; ++i) { - if (it == end || !is_hex_digit(peek())) { - throw JSONTokenizerError("Invalid Unicode sequence"); - } - hex += get(); - } - value += "\\u" + hex; - break; - } - default: - throw JSONTokenizerError("Invalid string sequence"); - } - - } else { - value += c; - } - } - // unterminated string sequence - throw JSONTokenizerError("Unterminated string"); -} - -} // namespace jqcpp::json diff --git a/src/pretty_printer.cpp b/src/pretty_printer.cpp deleted file mode 100644 index 09996d3..0000000 --- a/src/pretty_printer.cpp +++ /dev/null @@ -1,109 +0,0 @@ -/** - * @file - * @brief - */ - -#include "jqcpp/pretty_printer.hpp" -#include "jqcpp/json_value.hpp" -#include - -namespace jqcpp::json { - -/** - * @brief pretty print a json object - * - * @param value - * @param indent - * @return - */ -std::string JSONPrinter::print(const JSONValue &value, int indent) { - std::string out; - if (value.is_null()) { - return "null"; - } else if (value.is_bool()) { - return value.get_bool() ? "true" : "false"; - } else if (value.is_number()) { - std::ostringstream oss; - // oss << std::setprecision(std::numeric_limits::max_digits10) - // << value.get_number(); - oss << value.get_number(); - return oss.str(); - } else if (value.is_string()) { - return "\"" + value.get_string() + "\""; - } else if (value.is_array()) { - return print_array(value.get_array(), indent); - } else if (value.is_object()) { - return print_object(value.get_object(), indent); - } - return ""; -} - -/** - * @brief get a indent string, the default indent space for each - * level is 2 - * - * @param indent indent level - * @return - */ -std::string JSONPrinter::indent_string(int indent) { - return std::string(indent * 2, ' '); -} - -/** - * @brief pretty print out a object - * - * @param obj - * @param indent - * @return a string - * { - * "a": 1, - * "b": 2 - * } - */ -std::string JSONPrinter::print_object(const JSONObject &obj, int indent) { - if (obj.empty()) { - return "{}"; - } - std::ostringstream oss; - // print { - oss << "{\n"; - // output each key:value - bool first = true; - for (const auto &[key, value] : obj) { - //"key": value, - // if not the first key:value - // need to output a newline - if (!first) { - oss << ",\n"; - } - first = false; - oss << indent_string(indent + 1) << "\"" << key - << "\": " << print(value, indent + 1); - } - // after output all the key:values - // output the } - oss << "\n" << indent_string(indent) << "}"; - - return oss.str(); -} - -std::string JSONPrinter::print_array(const JSONArray &arr, int indent) { - if (arr.empty()) { - return "[]"; - } - - std::ostringstream oss; - oss << "[\n"; - bool first = true; - for (const auto &value : arr) { - if (!first) { - oss << ",\n"; - } - first = false; - oss << indent_string(indent + 1) << print(value, indent + 1); - } - oss << "\n" << indent_string(indent) << "]"; - return oss.str(); -} - -} // namespace jqcpp::json diff --git a/tests/test_expression_interpreter.cpp b/tests/test_expression_interpreter.cpp deleted file mode 100644 index 3147d97..0000000 --- a/tests/test_expression_interpreter.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "jqcpp/jq_interpreter.hpp" -#include "jqcpp/json_parser.hpp" -#include "jqcpp/json_value.hpp" -#include - -using jqcpp::JQInterpreter; -using jqcpp::json::JSONParser; -using jqcpp::json::JSONTokenizer; -using jqcpp::json::JSONValue; - -TEST_CASE("Expression parser handles simple expressions", "[parser]") { - JSONTokenizer tokenizer; - JSONParser parser; - - JSONValue input( - parser.parse(tokenizer.tokenize("{\"name\": \"John\",\"age\": 30.0}"))); - SECTION("Identity") { - JQInterpreter interpreter("."); - const auto &result = interpreter.execute(input); - CHECK(result.is_object()); - const auto &obj = result.get_object(); - CHECK(obj.size() == 2); - CHECK(obj[0].first == "name"); - CHECK(obj[0].second.get_string() == "John"); - CHECK(obj[1].first == "age"); - CHECK(obj[1].second.get_number() == 30.0); - } - - SECTION("Object access") { - JQInterpreter interpreter(".name"); - const auto &result = interpreter.execute(input); - CHECK(result.is_string()); - CHECK(result.get_string() == "John"); - } - - SECTION("Arithmetic") { - JQInterpreter interpreter(".age + 5"); - const auto &result = interpreter.execute(input); - CHECK(result.is_number()); - CHECK(result.get_number() == 35.0); - } -} - -TEST_CASE("Expression parser handles array operations", "[parser]") { - JSONTokenizer tokenizer; - JSONParser parser; - JSONValue input(parser.parse(tokenizer.tokenize("[1.0,2.0,3.0]"))); - - SECTION("Array access") { - JQInterpreter interpreter(".[1]"); - const auto &result = interpreter.execute(input); - CHECK(result.is_number()); - CHECK(result.get_number() == 2.0); - } - - SECTION("Array slice") { - JQInterpreter interpreter(".[1:3]"); - const auto &result = interpreter.execute(input); - CHECK(result.is_array()); - const auto &arr = result.get_array(); - CHECK(arr.size() == 2); - CHECK(arr[0].get_number() == 2.0); - CHECK(arr[1].get_number() == 3.0); - } -} diff --git a/tests/test_expression_tokenizer.cpp b/tests/test_expression_tokenizer.cpp deleted file mode 100644 index e36c3ef..0000000 --- a/tests/test_expression_tokenizer.cpp +++ /dev/null @@ -1,116 +0,0 @@ -#include "jqcpp/jq_lex.hpp" -#include - -using namespace jqcpp; - -TEST_CASE("JQLexer basic functionality", "[tokenizer]") { - JQLexer tokenizer; - - SECTION("Empty input") { - auto tokens = tokenizer.tokenize(""); - CHECK(tokens.size() == 1); - CHECK(tokens[0].type == TokenType::End); - } - - SECTION("Simple identifier") { - auto tokens = tokenizer.tokenize("abc"); - CHECK(tokens.size() == 2); - CHECK(tokens[0].type == TokenType::Identifier); - CHECK(tokens[0].value == "abc"); - CHECK(tokens[1].type == TokenType::End); - } - - SECTION("Number") { - auto tokens = tokenizer.tokenize("123.45"); - CHECK(tokens.size() == 2); - CHECK(tokens[0].type == TokenType::Number); - CHECK(tokens[0].value == "123.45"); - CHECK(tokens[1].type == TokenType::End); - } - - SECTION("String") { - auto tokens = tokenizer.tokenize("\"hello world\""); - CHECK(tokens.size() == 2); - CHECK(tokens[0].type == TokenType::String); - CHECK(tokens[0].value == "hello world"); - CHECK(tokens[1].type == TokenType::End); - } -} - -TEST_CASE("JQLexer complex expressions", "[tokenizer]") { JQLexer tokenizer; } - -TEST_CASE("JQLexer error handling", "[tokenizer]") { - JQLexer tokenizer; - - SECTION("Unterminated string") { - CHECK_THROWS_AS(tokenizer.tokenize("\"unterminated"), TokenizerError); - } -} - -TEST_CASE("JQLexer special cases", "[tokenizer]") { - JQLexer tokenizer; - - SECTION("Keywords") { - auto tokens = tokenizer.tokenize("and or not null true false"); - CHECK(tokens.size() == 7); - CHECK(tokens[0].type == TokenType::And); - CHECK(tokens[1].type == TokenType::Or); - CHECK(tokens[2].type == TokenType::Not); - CHECK(tokens[3].type == TokenType::Identifier); - CHECK(tokens[3].value == "null"); - CHECK(tokens[4].type == TokenType::Identifier); - CHECK(tokens[4].value == "true"); - CHECK(tokens[5].type == TokenType::Identifier); - CHECK(tokens[5].value == "false"); - } - - SECTION("Numbers with scientific notation") { - auto tokens = tokenizer.tokenize("1e10 2.5e-3"); - CHECK(tokens.size() == 3); - CHECK(tokens[0].type == TokenType::Number); - CHECK(tokens[0].value == "1e10"); - CHECK(tokens[1].type == TokenType::Number); - CHECK(tokens[1].value == "2.5e-3"); - } - - SECTION("Identifiers with underscores and dollars") { - auto tokens = tokenizer.tokenize("_var $var var_1"); - CHECK(tokens.size() == 4); - CHECK(tokens[0].type == TokenType::Identifier); - CHECK(tokens[0].value == "_var"); - CHECK(tokens[1].type == TokenType::Identifier); - CHECK(tokens[1].value == "$var"); - CHECK(tokens[2].type == TokenType::Identifier); - CHECK(tokens[2].value == "var_1"); - } -} - -TEST_CASE("JQLexer tokenizes expressions correctly", "[tokenizer]") { - JQLexer tokenizer; - - SECTION("Identifiers and numbers") { - auto tokens = tokenizer.tokenize("abc 123 _def45"); - CHECK(tokens.size() == 4); // Including End token - CHECK(tokens[0].type == TokenType::Identifier); - CHECK(tokens[0].value == "abc"); - CHECK(tokens[1].type == TokenType::Number); - CHECK(tokens[1].value == "123"); - CHECK(tokens[2].type == TokenType::Identifier); - CHECK(tokens[2].value == "_def45"); - } - - SECTION("Complex expression") { - auto tokens = tokenizer.tokenize(".users[0].name"); - CHECK(tokens.size() == 8); // Including End token - CHECK(tokens[0].type == TokenType::Dot); - CHECK(tokens[1].type == TokenType::Identifier); - CHECK(tokens[1].value == "users"); - CHECK(tokens[2].type == TokenType::LeftBracket); - CHECK(tokens[3].type == TokenType::Number); - CHECK(tokens[3].value == "0"); - CHECK(tokens[4].type == TokenType::RightBracket); - CHECK(tokens[5].type == TokenType::Dot); - CHECK(tokens[6].type == TokenType::Identifier); - CHECK(tokens[6].value == "name"); - } -} diff --git a/tests/test_jqcpp.cpp b/tests/test_jqcpp.cpp deleted file mode 100644 index e7f4696..0000000 --- a/tests/test_jqcpp.cpp +++ /dev/null @@ -1,490 +0,0 @@ -#include "jqcpp/jq_interpreter.hpp" -#include "jqcpp/json_parser.hpp" -#include "jqcpp/json_tokenizer.hpp" -#include "jqcpp/pretty_printer.hpp" -#include -#include - -using namespace jqcpp; - -// Helper function to run jqcpp and capture output -std::string run_jqcpp_test(const std::string &input, - const std::string &filter) { - std::istringstream iss(input); - std::ostringstream oss; - const char *argv[] = {"jqcpp", filter.c_str()}; - try { - int exit_code = run_jqcpp(2, const_cast(argv), iss, oss); - if (exit_code != 0) { - throw std::runtime_error("jqcpp exited with an error"); - } - return oss.str(); - } catch (...) { - throw std::runtime_error("Exception"); - } -} - -std::string pretty_json(const std::string &s) { - json::JSONTokenizer lexer; - json::JSONParser parser; - json::JSONPrinter printer; - return printer.print(parser.parse(lexer.tokenize(s))) + "\n"; -} - -TEST_CASE("JSON Parsing", "[json]") { - json::JSONTokenizer tokenizer; - json::JSONParser parser; - - SECTION("Parse simple JSON") { - std::string input = R"({"name": "John", "age": 30})"; - auto tokens = tokenizer.tokenize(input); - auto result = parser.parse(tokens); - CHECK(result.is_object()); - const auto &obj = result.get_object(); - CHECK(obj.size() == 2); - CHECK(obj[0].first == "name"); - CHECK(obj[0].second.get_string() == "John"); - CHECK(obj[1].first == "age"); - CHECK(obj[1].second.get_number() == 30); - } - - SECTION("Parse nested JSON") { - std::string input = - R"({"person": {"name": "John", "age": 30}, "scores": [95, 87, 92]})"; - auto tokens = tokenizer.tokenize(input); - auto result = parser.parse(tokens); - CHECK(result.is_object()); - const auto &obj = result.get_object(); - CHECK(obj.size() == 2); - CHECK(obj[0].first == "person"); - CHECK(obj[0].second.is_object()); - CHECK(obj[1].first == "scores"); - CHECK(obj[1].second.is_array()); - } -} - -TEST_CASE("Identity filter", "[filter]") { - std::string input = R"({"name": "John", "age": 30})"; - std::string filter = "."; - std::string expected = R"({ - "name": "John", - "age": 30 -} -)"; - CHECK(run_jqcpp_test(input, filter) == expected); -} - -TEST_CASE("Object property access", "[filter]") { - std::string input = R"({"name": "John", "age": 30})"; - - SECTION("Simple property access") { - std::string filter = ".name"; - std::string expected = "\"John\"\n"; - CHECK(run_jqcpp_test(input, filter) == expected); - } - - SECTION("Nested property access") { - input = R"({"person": {"name": "John", "age": 30}})"; - std::string filter = ".person.name"; - std::string expected = "\"John\"\n"; - CHECK(run_jqcpp_test(input, filter) == expected); - } -} - -TEST_CASE("Array element access", "[filter]") { - std::string input = R"(["a", "b", "c"])"; - - SECTION("Access by index") { - std::string filter = ".[1]"; - std::string expected = "\"b\"\n"; - CHECK(run_jqcpp_test(input, filter) == expected); - } - - // TODO: out of bound return a null - SECTION("Access out of bounds") { - std::string filter = ".[10]"; - std::string expected = "null\n"; - CHECK(run_jqcpp_test(input, filter) == expected); - } -} - -TEST_CASE("Array slicing", "[filter]") { - std::string input = R"([0, 1, 2, 3, 4])"; - - SECTION("Simple slice") { - std::string filter = ".[1:3]"; - std::string expected = R"([ - 1, - 2 -] -)"; - CHECK(run_jqcpp_test(input, filter) == expected); - } - - SECTION("Slice to end") { - std::string filter = ".[2:]"; - std::string expected = R"([ - 2, - 3, - 4 -] -)"; - CHECK(run_jqcpp_test(input, filter) == expected); - } - - SECTION("Slice from start") { - std::string filter = ".[:3]"; - std::string expected = R"([ - 0, - 1, - 2 -] -)"; - CHECK(run_jqcpp_test(input, filter) == expected); - } -} - -TEST_CASE("Arithmetic operations", "[filter]") { - std::string input = R"({"a": 5, "b": 3})"; - - SECTION("Addition") { - std::string filter = ".a + .b"; - std::string expected = "8\n"; - CHECK(run_jqcpp_test(input, filter) == expected); - } - - SECTION("Subtraction") { - std::string filter = ".a - .b"; - std::string expected = "2\n"; - CHECK(run_jqcpp_test(input, filter) == expected); - } -} - -// TEST_CASE("Length function", "[filter]") { -// SECTION("Array length") { -// std::string input = R"([1, 2, 3, 4, 5])"; -// std::string filter = "length"; -// std::string expected = "5\n"; -// CHECK(run_jqcpp_test(input, filter) == expected); -// } -// -// SECTION("String length") { -// std::string input = R"("Hello, World!")"; -// std::string filter = "length"; -// std::string expected = "13\n"; -// CHECK(run_jqcpp_test(input, filter) == expected); -// } -// -// SECTION("Object length") { -// std::string input = R"({"a": 1, "b": 2, "c": 3})"; -// std::string filter = "length"; -// std::string expected = "3\n"; -// CHECK(run_jqcpp_test(input, filter) == expected); -// } -// } -// -// TEST_CASE("Keys function", "[filter]") { -// std::string input = R"({"a": 1, "b": 2, "c": 3})"; -// std::string filter = "keys"; -// std::string expected = R"([ -// "a", -// "b", -// "c" -// ] -// )"; -// CHECK(run_jqcpp_test(input, filter) == expected); -// } - -TEST_CASE("Complex nested structure", "[filter]") { - std::string input = R"( -{ - "name": "John Doe", - "age": 30, - "address": { - "street": "123 Main St", - "city": "Anytown" - }, - "phones": [ - {"type": "home", "number": "555-1234"}, - {"type": "work", "number": "555-5678"} - ] -} -)"; - - SECTION("Nested object access") { - std::string filter = ".address.city"; - std::string expected = "\"Anytown\"\n"; - CHECK(run_jqcpp_test(input, filter) == expected); - } - - SECTION("Array of objects access") { - std::string filter = ".phones[1].number"; - std::string expected = "\"555-5678\"\n"; - CHECK(run_jqcpp_test(input, filter) == expected); - } -} - -// TEST_CASE("Error handling", "[filter]") { -// SECTION("Invalid JSON") { -// std::string input = R"({"name": "John", "age": })"; -// std::string filter = "."; -// CHECK_THROWS(run_jqcpp_test(input, filter)); -// } -// -// SECTION("Invalid filter") { -// std::string input = R"({"name": "John", "age": 30})"; -// std::string filter = ".invalid[]"; -// CHECK_THROWS(run_jqcpp_test(input, filter)); -// } -// } - -// TEST_CASE("Multiple outputs", "[filter]") { -// std::string input = R"(["a", "b", "c"])"; -// std::string filter = ".[]"; -// std::string expected = R"("a" -// "b" -// "c" -// )"; -// CHECK(run_jqcpp_test(input, filter) == expected); -// } - -// TEST_CASE("Pipe operator", "[filter]") { -// std::string input = R"({"a": [1, 2, 3], "b": [4, 5, 6]})"; -// std::string filter = ".a | map(. * 2)"; -// std::string expected = R"([ -// 2, -// 4, -// 6 -// ] -// )"; -// CHECK(run_jqcpp_test(input, filter) == expected); -// } - -// TEST_CASE("Object construction", "[filter]") { -// std::string input = R"({"name": "John", "age": 30})"; -// std::string filter = "{name: .name, is_adult: .age >= 18}"; -// std::string expected = R"({ -// "name": "John", -// "is_adult": true -// } -// )"; -// CHECK(run_jqcpp_test(input, filter) == expected); -// } - -// TEST_CASE("Array construction", "[filter]") { -// std::string input = R"({"a": 1, "b": 2, "c": 3})"; -// std::string filter = "[.a, .b, .c]"; -// std::string expected = R"([ -// 1, -// 2, -// 3 -// ] -// )"; -// CHECK(run_jqcpp_test(input, filter) == expected); -// } -// -// TEST_CASE("Conditional logic", "[filter]") { -// std::string input = R"({"temperature": 25})"; -// std::string filter = "if .temperature > 20 then \"warm\" else \"cool\" -// end"; std::string expected = "\"warm\"\n"; CHECK(run_jqcpp_test(input, -// filter) == expected); -// } -// - -TEST_CASE("JQ interpreter handles complex nested expressions", - "[jq][complex]") { - SECTION("Nested object access") { - std::string input = R"( - { - "foo": { - "bar": { - "baz": 42 - } - } - } - )"; - - CHECK(run_jqcpp_test(input, ".foo.bar.baz") == "42\n"); - } - - SECTION("Array slicing with nested object access") { - std::string input = R"( - { - "foo": [ - {"val": 1}, - {"val": 2}, - {"val": 3}, - {"val": 4}, - {"val": 5} - ] - } - )"; - - CHECK(run_jqcpp_test(input, ".foo[1:3]") == - pretty_json(R"([{"val":2}, {"val":3}])")); - } - - SECTION("Arithmetic with object and array access") { - std::string input = R"( - { - "foo": { - "bar": 10 - }, - "a": [5, 15, 25] - } - )"; - - CHECK(run_jqcpp_test(input, ".foo.bar + .a[1]") == "25\n"); - } - - SECTION("Complex nested expression with multiple operations") { - std::string input = R"( - { - "users": [ - {"name": "Alice", "age": 30}, - {"name": "Bob", "age": 25}, - {"name": "Charlie", "age": 35}, - {"name": "David", "age": 28} - ], - "threshold": 2 - } - )"; - - CHECK(run_jqcpp_test(input, ".users[1].age + .threshold") == "27\n"); - } - - SECTION("Combining length and arithmetic operations") { - std::string input = R"( - { - "items": [1, 2, 3, 4, 5], - "factor": 10 - } - )"; - - CHECK(run_jqcpp_test(input, "length") == pretty_json("2")); - } - - SECTION("Using keys with nested access") { - std::string input = R"( - { - "data": { - "x": 1, - "y": 2, - "z": 3 - } - } - )"; - - CHECK(run_jqcpp_test(input, R"(.data.x)") == pretty_json("1")); - } - - SECTION("Complex expression with multiple array and object accesses") { - std::string input = R"( - { - "teams": [ - {"name": "Red", "scores": [10, 20, 30]}, - {"name": "Blue", "scores": [15, 25, 35]}, - {"name": "Green", "scores": [12, 22, 32]} - ], - "bonus": 5 - } - )"; - - CHECK(run_jqcpp_test(input, ".teams[1].scores[1] + .bonus") == "30\n"); - } - - SECTION("Nested array slicing and object access") { - std::string input = R"( - [ - {"values": [1, 2, 3, 4, 5]}, - {"values": [6, 7, 8, 9, 10]}, - {"values": [11, 12, 13, 14, 15]} - ] - )"; - - CHECK(run_jqcpp_test(input, ".[1].values[2:4]") == pretty_json("[8,9]")); - } -} - -TEST_CASE("End-to-end jqcpp functionality tests", "[e2e]") { - SECTION("Identity filter") { - std::string input = R"({"name": "John", "age": 30})"; - std::string filter = "."; - std::string expected = pretty_json(input); - CHECK(run_jqcpp_test(input, filter) == expected); - } - - SECTION("Simple object property access") { - std::string input = R"({"name": "Alice", "age": 25})"; - std::string filter = ".name"; - std::string expected = "\"Alice\"\n"; - CHECK(run_jqcpp_test(input, filter) == expected); - } - - SECTION("Nested object property access") { - std::string input = R"({"user": {"name": "Bob", "details": {"age": 28}}})"; - std::string filter = ".user.details.age"; - std::string expected = "28\n"; - CHECK(run_jqcpp_test(input, filter) == expected); - } - - SECTION("Array element access") { - std::string input = R"(["apple", "banana", "cherry"])"; - std::string filter = ".[1]"; - std::string expected = "\"banana\"\n"; - CHECK(run_jqcpp_test(input, filter) == expected); - } - - SECTION("Array slicing") { - std::string input = R"([0, 1, 2, 3, 4, 5])"; - std::string filter = ".[2:5]"; - std::string expected = pretty_json("[2, 3, 4]"); - CHECK(run_jqcpp_test(input, filter) == expected); - } - - SECTION("Arithmetic operations") { - std::string input = R"({"x": 10, "y": 5})"; - std::string filter = ".x + .y"; - std::string expected = "15\n"; - CHECK(run_jqcpp_test(input, filter) == expected); - } - - SECTION("Complex nested structure") { - std::string input = R"( - { - "users": [ - {"name": "Alice", "scores": [85, 90, 95]}, - {"name": "Bob", "scores": [80, 85, 90]} - ] - })"; - std::string filter = ".users[1].scores[2]"; - std::string expected = "90\n"; - CHECK(run_jqcpp_test(input, filter) == expected); - } - - SECTION("Length function") { - std::string input = R"(["a", "b", "c", "d"])"; - std::string filter = "length"; - std::string expected = "4\n"; - CHECK(run_jqcpp_test(input, filter) == expected); - } - - SECTION("Keys function") { - std::string input = R"({"a": 1, "b": 2, "c": 3})"; - std::string filter = "keys"; - std::string expected = pretty_json(R"(["a", "b", "c"])"); - CHECK(run_jqcpp_test(input, filter) == expected); - } - - SECTION("Error handling - Invalid JSON") { - std::string input = R"({"name": "John", "age":})"; - std::string filter = "."; - CHECK_THROWS(run_jqcpp_test(input, filter)); - } - - SECTION("Error handling - Invalid filter") { - std::string input = R"({"name": "John", "age": 30})"; - std::string filter = ".invalid["; - CHECK_THROWS(run_jqcpp_test(input, filter)); - } -} diff --git a/tests/test_json_parser.cpp b/tests/test_json_parser.cpp deleted file mode 100644 index ac1870a..0000000 --- a/tests/test_json_parser.cpp +++ /dev/null @@ -1,240 +0,0 @@ -#include "jqcpp/json_parser.hpp" -#include -#include - -using namespace jqcpp::json; - -auto find_in_object(const JSONObject &obj, const std::string &key) { - return std::find_if(obj.begin(), obj.end(), - [&key](const auto &pair) { return pair.first == key; }); -} - -TEST_CASE("JSONParser parses simple types", "[parser]") { - JSONTokenizer tokenizer; - JSONParser parser; - - SECTION("Parse null") { - auto tokens = tokenizer.tokenize("null"); - auto result = parser.parse(tokens); - CHECK(result.is_null()); - } - - SECTION("Parse boolean") { - auto tokens = tokenizer.tokenize("true"); - auto result = parser.parse(tokens); - CHECK(result.is_bool()); - CHECK(result.get_bool() == true); - - tokens = tokenizer.tokenize("false"); - result = parser.parse(tokens); - CHECK(result.is_bool()); - CHECK(result.get_bool() == false); - } - - SECTION("Parse number") { - auto tokens = tokenizer.tokenize("123.45"); - auto result = parser.parse(tokens); - CHECK(result.is_number()); - CHECK_THAT(result.get_number(), Catch::Matchers::WithinAbs(123.45, 1e-10)); - } - - SECTION("Parse string") { - auto tokens = tokenizer.tokenize("\"Hello, World!\""); - auto result = parser.parse(tokens); - CHECK(result.is_string()); - CHECK(result.get_string() == "Hello, World!"); - } -} - -TEST_CASE("JSONParser parses arrays", "[parser]") { - JSONTokenizer tokenizer; - JSONParser parser; - - SECTION("Parse empty array") { - auto tokens = tokenizer.tokenize("[]"); - auto result = parser.parse(tokens); - CHECK(result.is_array()); - CHECK(result.get_array().empty()); - } - - SECTION("Parse simple array") { - auto tokens = tokenizer.tokenize("[1, 2, 3]"); - auto result = parser.parse(tokens); - CHECK(result.is_array()); - const auto &arr = result.get_array(); - CHECK(arr.size() == 3); - CHECK(arr[0].get_number() == 1); - CHECK(arr[1].get_number() == 2); - CHECK(arr[2].get_number() == 3); - } - - SECTION("Parse mixed-type array") { - auto tokens = tokenizer.tokenize(R"([1, "two", true, null])"); - auto result = parser.parse(tokens); - CHECK(result.is_array()); - const auto &arr = result.get_array(); - CHECK(arr.size() == 4); - CHECK(arr[0].get_number() == 1); - CHECK(arr[1].get_string() == "two"); - CHECK(arr[2].get_bool() == true); - CHECK(arr[3].is_null()); - } - - SECTION("Parse nested array") { - auto tokens = tokenizer.tokenize("[[1, 2], [3, 4]]"); - auto result = parser.parse(tokens); - CHECK(result.is_array()); - const auto &arr = result.get_array(); - CHECK(arr.size() == 2); - CHECK(arr[0].is_array()); - CHECK(arr[1].is_array()); - CHECK(arr[0].get_array()[0].get_number() == 1); - CHECK(arr[0].get_array()[1].get_number() == 2); - CHECK(arr[1].get_array()[0].get_number() == 3); - CHECK(arr[1].get_array()[1].get_number() == 4); - } -} - -TEST_CASE("JSONParser handles error cases", "[parser]") { - JSONTokenizer tokenizer; - JSONParser parser; - - SECTION("Unexpected end of tokens") { - auto tokens = tokenizer.tokenize("{"); - CHECK_THROWS_AS(parser.parse(tokens), JSONParserError); - } - - SECTION("Unexpected token type") { - auto tokens = tokenizer.tokenize("{]"); - CHECK_THROWS_AS(parser.parse(tokens), JSONParserError); - } - - SECTION("Missing colon in object") { - auto tokens = tokenizer.tokenize(R"({"key" "value"})"); - CHECK_THROWS_AS(parser.parse(tokens), JSONParserError); - } - - SECTION("Trailing comma in array") { - auto tokens = tokenizer.tokenize("[1, 2, 3,]"); - CHECK_THROWS_AS(parser.parse(tokens), JSONParserError); - } - - SECTION("Trailing comma in object") { - auto tokens = tokenizer.tokenize(R"({"a": 1, "b": 2,})"); - CHECK_THROWS_AS(parser.parse(tokens), JSONParserError); - } -} - -TEST_CASE("JSONParser parses objects", "[parser]") { - JSONTokenizer tokenizer; - JSONParser parser; - - SECTION("Parse empty object") { - auto tokens = tokenizer.tokenize("{}"); - auto result = parser.parse(tokens); - CHECK(result.is_object()); - CHECK(result.get_object().empty()); - } - - SECTION("Parse simple object") { - auto tokens = tokenizer.tokenize(R"({"name": "John", "age": 30})"); - auto result = parser.parse(tokens); - CHECK(result.is_object()); - const auto &obj = result.get_object(); - CHECK(obj.size() == 2); - auto name_it = find_in_object(obj, "name"); - REQUIRE(name_it != obj.end()); - CHECK(name_it->second.get_string() == "John"); - auto age_it = find_in_object(obj, "age"); - REQUIRE(age_it != obj.end()); - CHECK(age_it->second.get_number() == 30); - } - - SECTION("Parse nested object") { - auto tokens = - tokenizer.tokenize(R"({"person": {"name": "John", "age": 30}})"); - auto result = parser.parse(tokens); - CHECK(result.is_object()); - const auto &obj = result.get_object(); - CHECK(obj.size() == 1); - auto person_it = find_in_object(obj, "person"); - REQUIRE(person_it != obj.end()); - CHECK(person_it->second.is_object()); - const auto &person = person_it->second.get_object(); - auto name_it = find_in_object(person, "name"); - REQUIRE(name_it != person.end()); - CHECK(name_it->second.get_string() == "John"); - auto age_it = find_in_object(person, "age"); - REQUIRE(age_it != person.end()); - CHECK(age_it->second.get_number() == 30); - } -} - -TEST_CASE("JSONParser handles complex nested structures", "[parser]") { - JSONTokenizer tokenizer; - JSONParser parser; - - std::string json = R"({ - "name": "John Doe", - "age": 30, - "isStudent": false, - "grades": [95.5, 80.0, 88.5], - "address": { - "street": "123 Main St", - "city": "Anytown", - "zipCode": "12345" - }, - "phoneNumbers": [ - {"type": "home", "number": "555-1234"}, - {"type": "work", "number": "555-5678"} - ] - })"; - - auto tokens = tokenizer.tokenize(json); - auto result = parser.parse(tokens); - - CHECK(result.is_object()); - const auto &obj = result.get_object(); - - auto name_it = find_in_object(obj, "name"); - REQUIRE(name_it != obj.end()); - CHECK(name_it->second.get_string() == "John Doe"); - - auto age_it = find_in_object(obj, "age"); - REQUIRE(age_it != obj.end()); - CHECK(age_it->second.get_number() == 30); - - auto isStudent_it = find_in_object(obj, "isStudent"); - REQUIRE(isStudent_it != obj.end()); - CHECK(isStudent_it->second.get_bool() == false); - - auto grades_it = find_in_object(obj, "grades"); - REQUIRE(grades_it != obj.end()); - CHECK(grades_it->second.is_array()); - const auto &grades = grades_it->second.get_array(); - CHECK(grades.size() == 3); - CHECK_THAT(grades[0].get_number(), Catch::Matchers::WithinAbs(95.5, 1e-10)); - - auto address_it = find_in_object(obj, "address"); - REQUIRE(address_it != obj.end()); - CHECK(address_it->second.is_object()); - const auto &address = address_it->second.get_object(); - auto street_it = find_in_object(address, "street"); - REQUIRE(street_it != address.end()); - CHECK(street_it->second.get_string() == "123 Main St"); - auto zipCode_it = find_in_object(address, "zipCode"); - REQUIRE(zipCode_it != address.end()); - CHECK(zipCode_it->second.get_string() == "12345"); - - auto phoneNumbers_it = find_in_object(obj, "phoneNumbers"); - REQUIRE(phoneNumbers_it != obj.end()); - CHECK(phoneNumbers_it->second.is_array()); - const auto &phoneNumbers = phoneNumbers_it->second.get_array(); - CHECK(phoneNumbers.size() == 2); - auto type_it = find_in_object(phoneNumbers[0].get_object(), "type"); - REQUIRE(type_it != phoneNumbers[0].get_object().end()); - CHECK(type_it->second.get_string() == "home"); - auto number_it = find_in_object(phoneNumbers[1].get_object(), "number"); - REQUIRE(number_it != phoneNumbers[1].get_object().end()); - CHECK(number_it->second.get_string() == "555-5678"); -} diff --git a/tests/test_json_tokenizer.cpp b/tests/test_json_tokenizer.cpp deleted file mode 100644 index d957d09..0000000 --- a/tests/test_json_tokenizer.cpp +++ /dev/null @@ -1,166 +0,0 @@ -#include "jqcpp/json_tokenizer.hpp" -#include - -using namespace jqcpp::json; - -TEST_CASE("JSONTokenizer handles structural tokens", "[tokenizer]") { - JSONTokenizer tokenizer; - auto tokens = tokenizer.tokenize("{}[],:"); - - CHECK(tokens.size() == 6); - CHECK(tokens[0].type == TokenType::LeftBrace); - CHECK(tokens[1].type == TokenType::RightBrace); - CHECK(tokens[2].type == TokenType::LeftBracket); - CHECK(tokens[3].type == TokenType::RightBracket); - CHECK(tokens[4].type == TokenType::Comma); - CHECK(tokens[5].type == TokenType::Colon); -} - -TEST_CASE("JSONTokenizer handles strings", "[tokenizer]") { - JSONTokenizer tokenizer; - - SECTION("Simple string") { - auto tokens = tokenizer.tokenize(R"("Hello, World!")"); - CHECK(tokens.size() == 1); - CHECK(tokens[0].type == TokenType::String); - CHECK(tokens[0].value == "Hello, World!"); - } - - SECTION("String with escape sequences") { - auto tokens = tokenizer.tokenize(R"("\"\\\/\b\f\n\r\t")"); - CHECK(tokens.size() == 1); - CHECK(tokens[0].type == TokenType::String); - CHECK(tokens[0].value == "\"\\/\b\f\n\r\t"); - } - - SECTION("String with Unicode escape") { - auto tokens = tokenizer.tokenize(R"("\u00A9")"); - CHECK(tokens.size() == 1); - CHECK(tokens[0].type == TokenType::String); - CHECK(tokens[0].value == "\\u00A9"); - } -} - -TEST_CASE("JSONTokenizer handles numbers", "[tokenizer]") { - JSONTokenizer tokenizer; - - SECTION("Integer") { - auto tokens = tokenizer.tokenize("123"); - CHECK(tokens.size() == 1); - CHECK(tokens[0].type == TokenType::Number); - CHECK(tokens[0].value == "123"); - } - - SECTION("Negative integer") { - auto tokens = tokenizer.tokenize("-456"); - CHECK(tokens.size() == 1); - CHECK(tokens[0].type == TokenType::Number); - CHECK(tokens[0].value == "-456"); - } - - SECTION("Fraction") { - auto tokens = tokenizer.tokenize("123.456"); - CHECK(tokens.size() == 1); - CHECK(tokens[0].type == TokenType::Number); - CHECK(tokens[0].value == "123.456"); - } - - SECTION("Exponent") { - auto tokens = tokenizer.tokenize("1e-10"); - CHECK(tokens.size() == 1); - CHECK(tokens[0].type == TokenType::Number); - CHECK(tokens[0].value == "1e-10"); - } - - SECTION("Fraction and exponent") { - auto tokens = tokenizer.tokenize("123.456e+78"); - CHECK(tokens.size() == 1); - CHECK(tokens[0].type == TokenType::Number); - CHECK(tokens[0].value == "123.456e+78"); - } -} - -TEST_CASE("JSONTokenizer handles keywords", "[tokenizer]") { - JSONTokenizer tokenizer; - - SECTION("True") { - auto tokens = tokenizer.tokenize("true"); - CHECK(tokens.size() == 1); - CHECK(tokens[0].type == TokenType::True); - } - - SECTION("False") { - auto tokens = tokenizer.tokenize("false"); - CHECK(tokens.size() == 1); - CHECK(tokens[0].type == TokenType::False); - } - - SECTION("Null") { - auto tokens = tokenizer.tokenize("null"); - CHECK(tokens.size() == 1); - CHECK(tokens[0].type == TokenType::Null); - } -} - -TEST_CASE("JSONTokenizer handles whitespace", "[tokenizer]") { - JSONTokenizer tokenizer; - auto tokens = tokenizer.tokenize(" \n\r\t{ \n\r\t} \n\r\t"); - - CHECK(tokens.size() == 2); - CHECK(tokens[0].type == TokenType::LeftBrace); - CHECK(tokens[1].type == TokenType::RightBrace); -} - -TEST_CASE("JSONTokenizer handles complex JSON", "[tokenizer]") { - JSONTokenizer tokenizer; - std::string json = R"({ - "name": "John Doe", - "age": 30, - "isStudent": false, - "grades": [95.5, 80.0, 88.5], - "address": { - "street": "123 Main St", - "city": "Anytown" - }, - "phoneNumbers": [ - {"type": "home", "number": "555-1234"}, - {"type": "work", "number": "555-5678"} - ] - })"; - - auto tokens = tokenizer.tokenize(json); - - CHECK(tokens.size() == 59); -} - -TEST_CASE("JSONTokenizer handles error cases", "[tokenizer]") { - JSONTokenizer tokenizer; - - SECTION("Unterminated string") { - CHECK_THROWS_AS(tokenizer.tokenize("\"unterminated"), JSONTokenizerError); - } - - SECTION("Invalid escape sequence") { - CHECK_THROWS_AS(tokenizer.tokenize("\"\\x\""), JSONTokenizerError); - } - - SECTION("Invalid number format") { - CHECK_THROWS_AS(tokenizer.tokenize("12.34.56"), JSONTokenizerError); - } - - SECTION("Invalid token") { - CHECK_THROWS_AS(tokenizer.tokenize("invalid"), JSONTokenizerError); - } - - SECTION("Incomplete true") { - CHECK_THROWS_AS(tokenizer.tokenize("tru"), JSONTokenizerError); - } - - SECTION("Incomplete false") { - CHECK_THROWS_AS(tokenizer.tokenize("fals"), JSONTokenizerError); - } - - SECTION("Incomplete null") { - CHECK_THROWS_AS(tokenizer.tokenize("nul"), JSONTokenizerError); - } -} diff --git a/tests/test_pretty_printer.cpp b/tests/test_pretty_printer.cpp deleted file mode 100644 index e47f838..0000000 --- a/tests/test_pretty_printer.cpp +++ /dev/null @@ -1,194 +0,0 @@ -#include "jqcpp/json_parser.hpp" -#include "jqcpp/pretty_printer.hpp" -#include - -using namespace jqcpp::json; - -auto find_in_object(const JSONObject &obj, const std::string &key) { - return std::find_if(obj.begin(), obj.end(), - [&key](const auto &pair) { return pair.first == key; }); -} - -TEST_CASE("JSONPrinter handles simple types correctly", "[printer]") { - JSONParser parser; - JSONPrinter printer; - - SECTION("Null value") { - auto json = parser.parse(JSONTokenizer().tokenize("null")); - CHECK(printer.print(json) == "null"); - } - - SECTION("Boolean values") { - auto json_true = parser.parse(JSONTokenizer().tokenize("true")); - CHECK(printer.print(json_true) == "true"); - - auto json_false = parser.parse(JSONTokenizer().tokenize("false")); - CHECK(printer.print(json_false) == "false"); - } - - SECTION("Number values") { - auto json_int = parser.parse(JSONTokenizer().tokenize("42")); - CHECK(printer.print(json_int) == "42"); - - auto json_float = parser.parse(JSONTokenizer().tokenize("3.14159")); - CHECK(printer.print(json_float) == "3.14159"); - - auto json_negative = parser.parse(JSONTokenizer().tokenize("-273.15")); - CHECK(printer.print(json_negative) == "-273.15"); - } - - SECTION("String value") { - auto json = parser.parse(JSONTokenizer().tokenize("\"Hello, World!\"")); - CHECK(printer.print(json) == "\"Hello, World!\""); - } -} - -TEST_CASE("JSONPrinter handles empty structures correctly", "[printer]") { - JSONParser parser; - JSONPrinter printer; - - SECTION("Empty object") { - auto json = parser.parse(JSONTokenizer().tokenize("{}")); - CHECK(printer.print(json) == "{}"); - } - - SECTION("Empty array") { - auto json = parser.parse(JSONTokenizer().tokenize("[]")); - CHECK(printer.print(json) == "[]"); - } -} - -TEST_CASE("JSONPrinter formats objects correctly", "[printer]") { - JSONParser parser; - JSONPrinter printer; - - SECTION("Simple object") { - std::string input = R"({"name":"John","age":30})"; - auto json = parser.parse(JSONTokenizer().tokenize(input)); - std::string expected = "{\n \"name\": \"John\",\n \"age\": 30\n}"; - CHECK(printer.print(json) == expected); - } - - SECTION("Nested object") { - std::string input = R"({"person":{"name":"John","age":30}})"; - auto json = parser.parse(JSONTokenizer().tokenize(input)); - std::string expected = - "{\n \"person\": {\n \"name\": \"John\",\n \"age\": 30\n }\n}"; - CHECK(printer.print(json) == expected); - } -} - -TEST_CASE("JSONPrinter formats arrays correctly", "[printer]") { - JSONParser parser; - JSONPrinter printer; - - SECTION("Simple array") { - std::string input = "[1,2,3]"; - auto json = parser.parse(JSONTokenizer().tokenize(input)); - std::string expected = "[\n 1,\n 2,\n 3\n]"; - CHECK(printer.print(json) == expected); - } - - SECTION("Array of objects") { - std::string input = R"([{"x":1},{"y":2}])"; - auto json = parser.parse(JSONTokenizer().tokenize(input)); - std::string expected = - "[\n {\n \"x\": 1\n },\n {\n \"y\": 2\n }\n]"; - CHECK(printer.print(json) == expected); - } -} - -TEST_CASE("JSONPrinter handles complex nested structures", "[printer]") { - JSONParser parser; - JSONPrinter printer; - - std::string input = R"({ - "name": "John Doe", - "age": 30, - "isStudent": false, - "grades": [95.5, 80.0, 88.5], - "address": { - "street": "123 Main St", - "city": "Anytown", - "zipCode": "12345" - }, - "phoneNumbers": [ - {"type": "home", "number": "555-1234"}, - {"type": "work", "number": "555-5678"} - ] - })"; - - auto json = parser.parse(JSONTokenizer().tokenize(input)); - std::string expected = R"({ - "name": "John Doe", - "age": 30, - "isStudent": false, - "grades": [ - 95.5, - 80, - 88.5 - ], - "address": { - "street": "123 Main St", - "city": "Anytown", - "zipCode": "12345" - }, - "phoneNumbers": [ - { - "type": "home", - "number": "555-1234" - }, - { - "type": "work", - "number": "555-5678" - } - ] -})"; - - CHECK(printer.print(json) == expected); -} - -TEST_CASE("JSONPrinter formats objects correctly simple", "[printer]") { - JSONParser parser; - JSONPrinter printer; - - SECTION("Nested object") { - std::string input = R"({"person":{"name":"John","age":30}})"; - auto json = parser.parse(JSONTokenizer().tokenize(input)); - std::string output = printer.print(json); - - // Parse the output again to compare the structure, not the exact string - auto reparsed = parser.parse(JSONTokenizer().tokenize(output)); - - CHECK(json.is_object()); - CHECK(reparsed.is_object()); - - const auto &original_obj = json.get_object(); - const auto &reparsed_obj = reparsed.get_object(); - - auto original_person_it = find_in_object(original_obj, "person"); - CHECK(original_person_it != original_obj.end()); - CHECK(original_person_it->second.is_object()); - - auto reparsed_person_it = find_in_object(reparsed_obj, "person"); - CHECK(reparsed_person_it != reparsed_obj.end()); - CHECK(reparsed_person_it->second.is_object()); - - const auto &original_person = original_person_it->second.get_object(); - const auto &reparsed_person = reparsed_person_it->second.get_object(); - - auto original_name_it = find_in_object(original_person, "name"); - CHECK(original_name_it != original_person.end()); - auto reparsed_name_it = find_in_object(reparsed_person, "name"); - CHECK(reparsed_name_it != reparsed_person.end()); - CHECK(original_name_it->second.get_string() == - reparsed_name_it->second.get_string()); - - auto original_age_it = find_in_object(original_person, "age"); - CHECK(original_age_it != original_person.end()); - auto reparsed_age_it = find_in_object(reparsed_person, "age"); - CHECK(reparsed_age_it != reparsed_person.end()); - CHECK(original_age_it->second.get_number() == - reparsed_age_it->second.get_number()); - } -}