diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index ce675e3..6099556 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -3,29 +3,149 @@ name: CMake on: [push, pull_request] jobs: - build: - runs-on: ${{ matrix.os }} + ubuntu-address-undefined-coverage: + runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - build_type: [Debug, Release] + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y lcov + + - name: Configure CMake + run: cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON -DENABLE_UBSAN=ON -DENABLE_COV=ON + + - name: Build + run: cmake --build build --config Debug + + - name: Test with address sanitizer, undefined behavior sanitizer and coverage + working-directory: build + run: ctest -C Debug --rerun-failed --output-on-failure -T Test -T Coverage + + - name: Make coverage report + run: | + lcov --capture --directory build --output-file coverage.info + lcov --remove coverage.info '/usr/*' --output-file coverage.info + lcov --list coverage.info + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + ubuntu-memcheck: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y valgrind + + - name: Configure CMake + run: cmake -B build -DCMAKE_BUILD_TYPE=Debug + + - name: Build + run: cmake --build build --config Debug + + - name: Test with memory checker + working-directory: build + run: ctest -C Debug --rerun-failed --output-on-failure -T Test -T MemCheck + + windows-debug: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Configure CMake + run: cmake -B build -DCMAKE_BUILD_TYPE=Debug + + - name: Build + run: cmake --build build --config Debug + + - name: Test + working-directory: build + run: ctest -C Debug --rerun-failed --output-on-failure -T Test + + macos-address-undefined: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Configure CMake + run: cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON -DENABLE_UBSAN=ON + + - name: Build + run: cmake --build build --config Debug + + - name: Test with address sanitizer and undefined behavior sanitizer + working-directory: build + run: ctest -C Debug --rerun-failed --output-on-failure -T Test + + ubuntu-release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Configure CMake + run: cmake -B build -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build --config Release + + - name: Test + working-directory: build + run: ctest -C Release --rerun-failed --output-on-failure -T Test + + windows-release: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Configure CMake + run: cmake -B build -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build --config Release + + - name: Test + working-directory: build + run: ctest -C Release --rerun-failed --output-on-failure -T Test - env: - BUILD_TYPE: ${{ matrix.build_type }} + macos-release: + runs-on: macos-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - name: Configure CMake - run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} + run: cmake -B build -DCMAKE_BUILD_TYPE=Release - name: Build - run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + run: cmake --build build --config Release - name: Test - working-directory: ${{github.workspace}}/build - run: ctest -C ${{env.BUILD_TYPE}} --rerun-failed --output-on-failure -T Test -T Coverage + working-directory: build + run: ctest -C Release --rerun-failed --output-on-failure -T Test diff --git a/CMakeLists.txt b/CMakeLists.txt index bc7a02e..69733c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,67 +1,23 @@ -cmake_minimum_required(VERSION 3.0.0) -project(json VERSION 0.1.0) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=undefined") - -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) - -include(GenerateExportHeader) -include(GNUInstallDirs) -include(CMakePackageConfigHelpers) +cmake_minimum_required(VERSION 3.21) +project(json VERSION 0.2.0 LANGUAGES CXX) include(CTest) enable_testing() -option(JSON_INCLUDE_UTILS "Include utils library" ON) -if(JSON_INCLUDE_UTILS) +add_library(json src/json.cpp) +target_compile_features(json PUBLIC cxx_std_17) +target_include_directories(json PUBLIC $) +if(NOT TARGET utils) add_subdirectory(extern/utils) endif() - -file(GLOB JSON_SOURCES src/*.cpp) -file(GLOB JSON_HEADERS include/*.hpp) - -add_library(${PROJECT_NAME} SHARED ${JSON_SOURCES}) -add_dependencies(${PROJECT_NAME} utils) -GENERATE_EXPORT_HEADER(${PROJECT_NAME}) -target_include_directories(${PROJECT_NAME} PUBLIC $/include $) -target_link_libraries(${PROJECT_NAME} PUBLIC utils) +add_dependencies(json utils) +target_link_libraries(json PUBLIC utils) +setup_sanitizers(json) if(BUILD_TESTING) add_subdirectory(tests) endif() -if(MSVC) - target_compile_options(${PROJECT_NAME} PRIVATE /W4) -else() - target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic) - if (ADD_COVERAGE) - if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - target_compile_options(${PROJECT_NAME} PRIVATE --coverage) - target_link_libraries(${PROJECT_NAME} PUBLIC gcov) - endif() - endif() -endif() - -install( - TARGETS ${PROJECT_NAME} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} -) -install( - FILES ${JSON_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/json_export.h - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} -) -configure_package_config_file(src/${PROJECT_NAME}Config.cmake.in ${PROJECT_NAME}Config.cmake INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME} PATH_VARS CMAKE_INSTALL_INCLUDEDIR) -write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake VERSION 1.0.0 COMPATIBILITY SameMajorVersion) -install( - FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME} -) - -set(CPACK_PROJECT_NAME ${PROJECT_NAME}) +set(CPACK_PROJECT_NAME json) set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) include(CPack) diff --git a/README.md b/README.md index d0c4b83..0f92e20 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # JSON +![Build Status](https://github.com/ratioSolver/json/actions/workflows/cmake.yml/badge.svg) +[![codecov](https://codecov.io/gh/ratioSolver/json/branch/master/graph/badge.svg)](https://codecov.io/gh/ratioSolver/json) + This repository contains a [JSON](http://www.json.org/) parser and generator written in C++. ## Read a JSON from a string diff --git a/extern/utils b/extern/utils index 1fbfe27..7856090 160000 --- a/extern/utils +++ b/extern/utils @@ -1 +1 @@ -Subproject commit 1fbfe272a8c596235f3510295c759314d0b38474 +Subproject commit 7856090935c4a168c2921fa29d005edb3b286eae diff --git a/include/json.hpp b/include/json.hpp index 34283b1..78bf5c1 100644 --- a/include/json.hpp +++ b/include/json.hpp @@ -1,11 +1,12 @@ #pragma once -#include "json_export.h" #include +#include +#include #include #include -#include -#include +#include +#include namespace json { @@ -19,456 +20,721 @@ namespace json object }; + /** + * @brief A class representing a JSON value. + * + * The `json` class is a versatile container that can hold different types of JSON values, including null, boolean, number, string, array, and object. + * It provides various member functions and overloaded operators for accessing and manipulating JSON values. + */ class json { - private: - json(std::map &obj) : type(json_type::object), obj_val(std::move(obj)) {} - json(std::vector &arr) : type(json_type::array), arr_val(std::move(arr)) {} - public: - json(json_type type = json_type::object) : type(type) {} - json(std::nullptr_t) : type(json_type::null) {} - json(const std::string &str, bool is_number = false) : type(is_number ? json_type::number : json_type::string), str_val(str) {} - json(const char *str, bool is_number = false) : type(is_number ? json_type::number : json_type::string), str_val(str) {} - json(bool b) : type(json_type::boolean), bool_val(b) {} - template ::value, T>::type> - json(T t) : type(json_type::number), str_val(std::to_string(t)) {} - json(std::map &&obj) : type(json_type::object), obj_val(std::move(obj)) {} - json(std::vector &&arr) : type(json_type::array), arr_val(std::move(arr)) {} - json(std::initializer_list list) : type(json_type::array) + /** + * @brief Constructs a JSON object with the specified type. + * + * This constructor initializes a JSON object with the specified type. The type can be one of the following: + * - json_type::null: Initializes the JSON object with a null value. + * - json_type::boolean: Initializes the JSON object with a boolean value (false). + * - json_type::number: Initializes the JSON object with a numeric value (0). + * - json_type::string: Initializes the JSON object with an empty string. + * - json_type::array: Initializes the JSON object with an empty array. + * - json_type::object: Initializes the JSON object with an empty object. + * + * @param type The type of the JSON object to be constructed. + */ + json(json_type type = json_type::object) noexcept : value(nullptr) { - if (list.size() == 2 && list.begin()->type == json_type::string) - { - type = json_type::object; - obj_val.emplace(list.begin()->str_val, *(list.begin() + 1)); - } - else if (list.begin()->type == json_type::object) + switch (type) { - type = json_type::object; - for (auto &p : list) - for (auto &q : p.obj_val) - obj_val.emplace(q.first, q.second); + case json_type::null: + value = nullptr; + break; + case json_type::boolean: + value = false; + break; + case json_type::number: + value = static_cast(0); + break; + case json_type::string: + value = std::string(); + break; + case json_type::array: + value = std::vector(); + break; + case json_type::object: + value = std::map>(); + break; } - else - for (auto &p : list) - arr_val.emplace_back(p); } - json(const json &other) : type(other.type), bool_val(other.bool_val), str_val(other.str_val), obj_val(other.obj_val), arr_val(other.arr_val) {} - json(json &&other) : type(other.type), bool_val(other.bool_val), str_val(std::move(other.str_val)), obj_val(std::move(other.obj_val)), arr_val(std::move(other.arr_val)) { other.type = json_type::null; } - json &operator=(const json &other) - { - type = other.type; - bool_val = other.bool_val; - str_val = other.str_val; - obj_val = other.obj_val; - arr_val = other.arr_val; - return *this; - } - json &operator=(json &&other) - { - type = other.type; - bool_val = other.bool_val; - str_val = std::move(other.str_val); - obj_val = std::move(other.obj_val); - arr_val = std::move(other.arr_val); - return *this; - } - json &operator=(const std::string &str) - { - set_type(json_type::string); - str_val = str; - return *this; - } - json &operator=(const char *str) - { - set_type(json_type::string); - str_val = str; - return *this; - } - json &operator=(bool b) - { - set_type(json_type::boolean); - bool_val = b; - return *this; - } - template ::value, T>::type> - json &operator=(T t) + /** + * @brief Copy constructor for the json class. + * + * This constructor creates a new json object as a copy of another json object. + * + * @param other The json object to be copied. + */ + json(const json &other) : value(other.value) {} + /** + * @brief Move constructor for the json class. + * + * This constructor transfers ownership of the value from another json object to the new json object. + * + * @param other The json object to be moved. + */ + json(json &&other) noexcept : value(std::move(other.value)) { other.value = nullptr; } + + /** + * @brief Constructs a JSON object from a value of type T. + * + * This constructor allows you to create a `json` object from any type that can be converted to a JSON value. + * The type T must be convertible to one of the supported JSON types (null, boolean, number, string, array, or object). + * + * @param v The value to be converted to a JSON object. + */ + template , json>, int> = 0> + json(T &&v) : value(std::forward(v)) {} + + /** + * @brief Constructs a `json` object from an initializer list of `json` objects. + * + * This constructor allows you to create a `json` object by providing an initializer list + * of `json` objects. The elements in the initializer list will be used to initialize the + * `json` object in the same order as they appear in the initializer list. + * + * @param init An initializer list of `json` objects. + */ + json(std::initializer_list init); + + /** + * @brief Assignment operator for the json class. + * + * This operator assigns the value of another json object to the current object. + * + * @param other The json object to be assigned. + * @return A reference to the current json object after assignment. + */ + json &operator=(const json &other) noexcept { - set_type(json_type::number); - str_val = std::to_string(t); + value = other.value; return *this; } - json &operator=(std::nullptr_t) + + /** + * @brief Assignment operator for the json class. + * + * This operator allows you to assign a value of type T to the current json object. + * The type T must be convertible to one of the supported JSON types (null, boolean, number, string, array, or object). + * + * @param v The value to be assigned to the json object. + * @return A reference to the current json object after assignment. + */ + template , json>, int> = 0> + json &operator=(T &&v) { - set_type(json_type::null); + value = std::forward(v); return *this; } - bool has(const std::string &key) const { return type == json_type::object && obj_val.find(key) != obj_val.end(); } - bool has(const char *key) const { return type == json_type::object && obj_val.find(key) != obj_val.end(); } - bool has(size_t index) const { return type == json_type::array && index < arr_val.size(); } + /** + * @brief Accesses the JSON value associated with the given key. + * + * This operator allows access to the JSON object element corresponding to the specified key. + * If the key does not exist, a new element is created and returned. + * + * @tparam Key Type of the key, which must be convertible to std::string. + * @param key The key to access in the JSON object. + * @return Reference to the JSON value associated with the key. + */ + template , const char *> || std::is_same_v, std::string> || std::is_same_v, std::string_view>, int> = 0> + json &operator[](Key &&key) { return std::get>>(value)[std::string(std::forward(key))]; } - json &operator[](const std::string &key) - { - set_type(json_type::object); - return obj_val[key]; - } - const json &operator[](const std::string &key) const { return obj_val.at(key); } - json &operator[](const char *key) - { - set_type(json_type::object); - return obj_val[key]; - } - const json &operator[](const char *key) const { return obj_val.at(key); } - json &operator[](int index) - { - set_type(json_type::array); - return arr_val[index]; - } - const json &operator[](int index) const { return arr_val[index]; } - json &operator[](size_t index) - { - set_type(json_type::array); - return arr_val[index]; - } - const json &operator[](size_t index) const { return arr_val.at(index); } + /** + * @brief Accesses the JSON value associated with the given key. + * + * This operator allows access to the JSON object element corresponding to the specified key. + * If the key does not exist, an exception is thrown. + * + * @tparam Key Type of the key, which must be convertible to std::string. + * @param key The key to access in the JSON object. + * @return A constant reference to the JSON value associated with the key. + */ + template , const char *> || std::is_same_v, std::string> || std::is_same_v, std::string_view>, int> = 0> + const json &operator[](Key &&key) const { return std::get>>(value).at(std::string(std::forward(key))); } + + /** + * @brief Accesses the JSON value at the specified index. + * + * This operator allows access to the JSON array element at the specified index. + * If the index is out of bounds, an exception is thrown. + * + * @tparam Index Type of the index, which must be an integral type. + * @param idx The index to access in the JSON array. + * @return Reference to the JSON value at the specified index. + */ + template , int> = 0> + json &operator[](Index idx) { return std::get>(value)[static_cast(idx)]; } + + /** + * @brief Accesses the JSON value at the specified index. + * + * This operator allows access to the JSON array element at the specified index. + * If the index is out of bounds, an exception is thrown. + * + * @tparam Index Type of the index, which must be an integral type. + * @param idx The index to access in the JSON array. + * @return A constant reference to the JSON value at the specified index. + */ + template , int> = 0> + const json &operator[](Index idx) const { return std::get>(value).at(static_cast(idx)); } + + [[nodiscard]] bool operator==(const json &other) const noexcept { return value == other.value; } + [[nodiscard]] bool operator!=(const json &other) const noexcept { return !(*this == other); } + + /** + * @brief Checks if the JSON value is null. + * + * @return true if the JSON value is null, false otherwise. + */ + [[nodiscard]] constexpr bool is_null() const noexcept { return value.index() == 0; } + /** + * @brief Checks if the JSON value is a boolean. + * + * @return true if the JSON value is a boolean, false otherwise. + */ + [[nodiscard]] constexpr bool is_boolean() const noexcept { return value.index() == 1; } + /** + * @brief Checks if the JSON value is an integer. + * + * @return true if the JSON value is an integer, false otherwise. + */ + [[nodiscard]] constexpr bool is_integer() const noexcept { return value.index() == 2; } + /** + * @brief Checks if the JSON value is an unsigned integer. + * + * @return true if the JSON value is an unsigned integer, false otherwise. + */ + [[nodiscard]] constexpr bool is_unsigned() const noexcept { return value.index() == 3; } + /** + * @brief Checks if the JSON value is a floating-point number. + * + * @return true if the JSON value is a floating-point number, false otherwise. + */ + [[nodiscard]] constexpr bool is_float() const noexcept { return value.index() == 4; } + /** + * @brief Checks if the JSON value is a number. + * + * This function checks if the JSON value is of type integer, unsigned integer, or floating-point number. + * + * @return true if the JSON value is a number, false otherwise. + */ + [[nodiscard]] constexpr bool is_number() const noexcept { return value.index() == 2 || value.index() == 3 || value.index() == 4; } + /** + * @brief Checks if the JSON value is a string. + * + * @return true if the JSON value is a string, false otherwise. + */ + [[nodiscard]] constexpr bool is_string() const noexcept { return value.index() == 5; } + /** + * @brief Checks if the JSON value is an object. + * + * @return true if the JSON value is an object, false otherwise. + */ + [[nodiscard]] constexpr bool is_object() const noexcept { return value.index() == 6; } + /** + * @brief Checks if the JSON value is an array. + * + * @return true if the JSON value is an array, false otherwise. + */ + [[nodiscard]] constexpr bool is_array() const noexcept { return value.index() == 7; } - bool operator==(const json &other) const + /** + * @brief Checks if the JSON value is a primitive type. + * + * This function checks if the JSON value is of a primitive type, which includes null, boolean, integer, unsigned integer, + * floating-point number, or string. + * + * @return true if the JSON value is a primitive type, false otherwise. + */ + [[nodiscard]] constexpr bool is_primitive() const noexcept { return is_null() || is_boolean() || is_integer() || is_unsigned() || is_float() || is_string(); } + /** + * @brief Checks if the JSON value is a structured type. + * + * This function checks if the JSON value is either an object or an array. + * + * @return true if the JSON value is a structured type (object or array), false otherwise. + */ + [[nodiscard]] constexpr bool is_structured() const noexcept { return is_object() || is_array(); } + + /** + * @brief Returns the type of the JSON value. + * + * This function returns the type of the JSON value as an enum value of the `json_type` enumeration. + * The possible types are: null, boolean, number, string, object, and array. + * + * @return The type of the JSON value. + */ + [[nodiscard]] constexpr json_type get_type() const noexcept { - if (type != other.type) - return false; - switch (type) + switch (value.index()) { - case json_type::null: - return true; - case json_type::string: - return str_val == other.str_val; - case json_type::number: - return str_val == other.str_val; - case json_type::boolean: - return bool_val == other.bool_val; - case json_type::array: - return arr_val == other.arr_val; - case json_type::object: - return obj_val == other.obj_val; - default: - return false; + case 0: + return json_type::null; + case 1: + return json_type::boolean; + case 2: + case 3: + case 4: + return json_type::number; + case 5: + return json_type::string; + case 6: + return json_type::object; + case 7: + return json_type::array; + default: // should never happen + return json_type::null; } } - bool operator!=(const json &other) const { return !(*this == other); } - - bool operator==(const std::string &str) const { return type == json_type::string && str_val == str; } - bool operator!=(const std::string &str) const { return !(*this == str); } - bool operator==(const char *str) const { return type == json_type::string && str_val == str; } - bool operator!=(const char *str) const { return !(*this == str); } - bool operator==(bool b) const { return type == json_type::boolean && bool_val == b; } - bool operator!=(bool b) const { return type != json_type::boolean || bool_val != b; } - bool operator==(int i) const { return type == json_type::number && std::stoi(str_val) == i; } - bool operator!=(int i) const { return !(*this == i); } - bool operator==(double d) const { return type == json_type::number && std::stod(str_val) == d; } - bool operator!=(double d) const { return !(*this == d); } - bool operator==(long l) const { return type == json_type::number && std::stol(str_val) == l; } - bool operator!=(long l) const { return !(*this == l); } - bool operator==(unsigned long l) const { return type == json_type::number && std::stoul(str_val) == l; } - bool operator!=(unsigned long l) const { return !(*this == l); } - bool operator==(std::nullptr_t) const { return type == json_type::null; } - bool operator!=(std::nullptr_t) const { return type != json_type::null; } - - json_type get_type() const { return type; } - size_t size() const + + /** + * Returns the size of the JSON value. + * For arrays, it returns the number of elements. + * For objects, it returns the number of key-value pairs. + * For other types, it returns 0. + * + * @return The size of the JSON value. + */ + [[nodiscard]] size_t size() const { - switch (type) + switch (get_type()) { - case json_type::null: - return 0; - case json_type::string: - return str_val.size(); - case json_type::number: - return str_val.size(); - case json_type::boolean: - return 1; case json_type::array: - return arr_val.size(); + return std::get>(value).size(); case json_type::object: - return obj_val.size(); + return std::get>>(value).size(); default: return 0; } } - operator bool() const + /** + * Checks if the JSON object contains a specific key. + * + * @param key The key to check for. + * @return True if the key is present in the JSON object, false otherwise. + */ + [[nodiscard]] bool contains(std::string_view key) const { return is_object() && std::get>>(value).count(key) > 0; } + + [[nodiscard]] bool operator==(std::nullptr_t) const noexcept { return value.index() == 0; } + + template , int> = 0> + [[nodiscard]] bool operator==(T num) const noexcept { - switch (type) + switch (value.index()) { - case json_type::null: - return false; - case json_type::string: - return !str_val.empty(); - case json_type::number: - return str_val != "0"; - case json_type::boolean: - return bool_val; - case json_type::array: - return !arr_val.empty(); - case json_type::object: - return !obj_val.empty(); + case 1: + return std::get(value) == static_cast(num); + case 2: + return std::get(value) == static_cast(num); + case 3: + return std::get(value) == static_cast(num); + case 4: + return std::get(value) == static_cast(num); default: return false; } } - operator int() const + + template , std::string> || std::is_same_v, std::string_view> || std::is_same_v, const char *>, int> = 0> + [[nodiscard]] bool operator==(T &&str) const noexcept { return value.index() == 5 && std::get(value) == std::string(str); } + + [[nodiscard]] bool operator!=(std::nullptr_t) const noexcept { return value.index() != 0; } + + template , int> = 0> + [[nodiscard]] bool operator!=(T num) const noexcept { - switch (type) + switch (value.index()) { - case json_type::null: - return 0; - case json_type::string: - return std::stoi(str_val); - case json_type::number: - return std::stoi(str_val); - case json_type::boolean: - return bool_val ? 1 : 0; - case json_type::array: - return static_cast(arr_val.size()); - case json_type::object: - return static_cast(obj_val.size()); + case 1: + return std::get(value) != static_cast(num); + case 2: + return std::get(value) != static_cast(num); + case 3: + return std::get(value) != static_cast(num); + case 4: + return std::get(value) != static_cast(num); default: - return 0; + return true; } } - operator double() const + + template , std::string> || std::is_same_v, std::string_view> || std::is_same_v, const char *>, int> = 0> + [[nodiscard]] bool operator!=(T &&str) const noexcept { return value.index() != 5 || std::get(value) != std::string(str); } + + /** + * @brief Get the JSON value as the specified type T. + * + * This templated function provides explicit conversion of the JSON value to type T. + * Use this when you want to be explicit about the conversion or when implicit + * conversion operators might be ambiguous. + * + * @tparam T The type to convert the JSON value to. + * @return The JSON value converted to type T. + */ + template + [[nodiscard]] T get() const noexcept { - switch (type) - { - case json_type::null: - return 0; - case json_type::string: - return std::stod(str_val); - case json_type::number: - return std::stod(str_val); - case json_type::boolean: - return bool_val ? 1 : 0; - case json_type::array: - return static_cast(arr_val.size()); - case json_type::object: - return static_cast(obj_val.size()); - default: - return 0; - } + if constexpr (std::is_same_v) + return static_cast(*this); + else if constexpr (std::is_integral_v && std::is_signed_v) + return static_cast(*this); + else if constexpr (std::is_integral_v && std::is_unsigned_v) + return static_cast(*this); + else if constexpr (std::is_floating_point_v) + return static_cast(*this); + else if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) + return static_cast(*this); + else if constexpr (std::is_same_v>>) + return static_cast>>(*this); + else if constexpr (std::is_same_v>) + return static_cast>(*this); + else + static_assert(std::is_arithmetic_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v>> || std::is_same_v>, "Unsupported type for json::get()"); } - operator long() const + + /** + * @brief Conversion operator to bool. + * + * This operator converts the JSON value to a boolean value. + * The conversion is based on the type of the JSON value. + * + * @return true if the JSON value is considered true, false otherwise. + */ + operator bool() const noexcept { - switch (type) + switch (value.index()) { - case json_type::null: - return 0; - case json_type::string: - return std::stol(str_val); - case json_type::number: - return std::stol(str_val); - case json_type::boolean: - return bool_val ? 1 : 0; - case json_type::array: - return static_cast(arr_val.size()); - case json_type::object: - return static_cast(obj_val.size()); + case 1: + return std::get(value); + case 2: + return std::get(value) != 0; + case 3: + return std::get(value) != 0; + case 4: + return std::get(value) != 0.0; + case 5: + return !std::get(value).empty(); + case 6: + return !std::get>>(value).empty(); + case 7: + return !std::get>(value).empty(); default: - return 0; + return false; } } - operator unsigned long() const + + /** + * @brief Conversion operator to int. + * + * This operator converts the JSON value to an integer value. + * The conversion is based on the type of the JSON value. + * + * @return The integer value of the JSON object. + */ + operator int64_t() const noexcept { - switch (type) + switch (value.index()) { - case json_type::null: - return 0; - case json_type::string: - return std::stoul(str_val); - case json_type::number: - return std::stoul(str_val); - case json_type::boolean: - return bool_val ? 1 : 0; - case json_type::array: - return static_cast(arr_val.size()); - case json_type::object: - return static_cast(obj_val.size()); + case 1: + return std::get(value) ? 1 : 0; + case 2: + return std::get(value); + case 3: + return std::get(value); + case 4: + return static_cast(std::get(value)); default: return 0; } } - operator std::string() const + + /** + * @brief Conversion operator to unsigned 64-bit integer. + * + * This operator converts the JSON value to an unsigned 64-bit integer value. + * The conversion is based on the type of the JSON value. + * + * @return The unsigned 64-bit integer value of the JSON object. + */ + operator uint64_t() const noexcept { - switch (type) + switch (value.index()) { - case json_type::null: - return ""; - case json_type::string: - return str_val; - case json_type::number: - return str_val; - case json_type::boolean: - return bool_val ? "true" : "false"; - case json_type::array: - return std::to_string(arr_val.size()); - case json_type::object: - return std::to_string(obj_val.size()); + case 1: + return std::get(value) ? 1 : 0; + case 2: + return std::get(value); + case 3: + return std::get(value); + case 4: + return static_cast(std::get(value)); default: - return ""; + return 0; } } - operator std::map() const + + /** + * @brief Conversion operator to double. + * + * This operator converts the JSON value to a double value. + * The conversion is based on the type of the JSON value. + * + * @return The double value of the JSON object. + */ + operator double() const noexcept { - switch (type) + switch (value.index()) { - case json_type::null: - return {}; - case json_type::string: - return {}; - case json_type::number: - return {}; - case json_type::boolean: - return {}; - case json_type::array: - return {}; - case json_type::object: - return obj_val; + case 1: + return std::get(value) ? 1.0 : 0.0; + case 2: + return static_cast(std::get(value)); + case 3: + return static_cast(std::get(value)); + case 4: + return std::get(value); default: - return {}; + return 0.0; } } - operator std::vector() const + + /** + * @brief Conversion operator to string. + * + * This operator converts the JSON value to a string. + * The conversion is based on the type of the JSON value. + * + * @return The string value of the JSON object. + */ + operator std::string() const noexcept { - switch (type) + switch (value.index()) { - case json_type::null: - return {}; - case json_type::string: - return {}; - case json_type::number: - return {}; - case json_type::boolean: - return {}; - case json_type::array: - return arr_val; - case json_type::object: - return {}; + case 1: + return std::get(value) ? "true" : "false"; + case 2: + return std::to_string(std::get(value)); + case 3: + return std::to_string(std::get(value)); + case 4: + return std::to_string(std::get(value)); + case 5: + return std::get(value); default: - return {}; + return ""; } } - std::map &get_object() - { - set_type(json_type::object); - return obj_val; - } - const std::map &get_object() const { return obj_val; } - std::vector &get_array() - { - set_type(json_type::array); - return arr_val; - } - const std::vector &get_array() const { return arr_val; } + /** + * @brief Conversion operator to std::map>. + * + * This operator converts the JSON value to a std::map>. + * The conversion is based on the type of the JSON value. + * + * @return The std::map> value of the JSON object. + */ + operator std::map>() const noexcept { return value.index() == 6 ? std::get>>(value) : std::map>(); } + /** + * Returns a constant reference to the underlying map object if the JSON value is an object. + * + * @return A constant reference to the underlying map object. + */ + [[nodiscard]] const std::map> &as_object() const noexcept { return std::get>>(value); } - void set(const std::string &key, json &&j) - { - set_type(json_type::object); - obj_val[key] = std::move(j); - } + /** + * @brief Conversion operator to std::vector. + * + * This operator converts the JSON value to a std::vector. + * The conversion is based on the type of the JSON value. + * + * @return The std::vector value of the JSON object. + */ + operator std::vector() const noexcept { return value.index() == 7 ? std::get>(value) : std::vector(); } + /** + * Returns a const reference to the internal vector representation of the JSON value. + * + * @return const std::vector& A const reference to the internal vector representation of the JSON value. + */ + [[nodiscard]] const std::vector &as_array() const noexcept { return std::get>(value); } - void push_back(json &&j) - { - set_type(json_type::array); - arr_val.push_back(std::move(j)); - } + /** + * @brief Clears the JSON object. + * + * This function clears the JSON object by setting its value to null. + */ + void clear() noexcept { value = nullptr; } - std::string to_string() const - { - std::stringstream ss; - dump(ss); - return ss.str(); - } + /** + * @brief Adds a new element to the end of the JSON array. + * + * This function appends a new element to the end of the JSON array. The element + * to be added should be of the same type as the elements in the array. + * + * @param val The value to be added to the array. + */ + void push_back(const json &val) { std::get>(value).push_back(val); } + /** + * @brief Adds a new element to the end of the JSON array. + * + * This function appends a new element to the end of the JSON array. The element + * to be added should be of the same type as the elements in the array. + * + * @param val The value to be added to the array. + */ + void push_back(json &&val) { std::get>(value).push_back(std::move(val)); } - friend std::ostream &operator<<(std::ostream &os, const json &j) - { - j.dump(os); - return os; - } + /** + * @brief Erases the element with the specified key from the JSON object. + * + * This function removes the element with the specified key from the JSON object. + * If the key does not exist, no changes are made. + * + * @param key The key of the element to be erased. + */ + void erase(std::string_view key) { std::get>>(value).erase(key.data()); } - private: - void set_type(json_type tp) + /** + * @brief Erases an element at the specified index from the JSON array. + * + * This function removes the element at the specified index from the JSON array. + * The index must be a valid position within the array. + * + * @param index The index of the element to be erased. + */ + void erase(size_t index) { std::get>(value).erase(std::get>(value).begin() + index); } + + /** + * Returns a string representation of the JSON value. + * + * The `dump` function converts the JSON value into a string representation. + * The resulting string represents the JSON value according to its type: + * - For a null value, the string "null" is returned. + * - For a boolean value, the string "true" or "false" is returned. + * - For an integer value, the string representation of the integer is returned. + * - For a floating-point value, the string representation of the floating-point number is returned. + * - For a string value, the string is enclosed in double quotes and returned. + * - For an object value, the string representation of the object is returned. + * - For an array value, the string representation of the array is returned. + * + * @return A string representation of the JSON value. + */ + [[nodiscard]] std::string dump() const { - if (tp != type) + switch (value.index()) + { + case 0: // null + return "null"; + case 1: // boolean + return std::get(value) ? "true" : "false"; + case 2: // int64_t + return std::to_string(std::get(value)); + case 3: // uint64_t + return std::to_string(std::get(value)); + case 4: // double + return std::to_string(std::get(value)); + case 5: // string { - bool_val = false; - str_val = tp == json_type::string ? "" : "0"; - obj_val.clear(); - arr_val.clear(); - type = tp; + std::string escaped; + escaped.reserve(std::get(value).size()); + for (char c : std::get(value)) + switch (c) + { + case '"': + escaped += "\\\""; + break; + case '\\': + escaped += "\\\\"; + break; + case '\b': + escaped += "\\b"; + break; + case '\f': + escaped += "\\f"; + break; + case '\n': + escaped += "\\n"; + break; + case '\r': + escaped += "\\r"; + break; + case '\t': + escaped += "\\t"; + break; + default: + escaped += c; + break; + } + return "\"" + escaped + "\""; } - } - - void dump(std::ostream &os) const - { - switch (type) + case 6: // object { - case json_type::null: - os << "null"; - break; - case json_type::string: - os << "\"" << str_val << "\""; - break; - case json_type::number: - os << str_val; - break; - case json_type::boolean: - os << (bool_val ? "true" : "false"); - break; - case json_type::array: - os << "["; - for (size_t i = 0; i < arr_val.size(); i++) + std::string str = "{"; + const auto &m = std::get>>(value); + for (auto it = m.begin(); it != m.end(); ++it) { - arr_val[i].dump(os); - if (i != arr_val.size() - 1) - os << ","; + if (it != m.begin()) + str += ","; + str += "\"" + it->first + "\":" + it->second.dump(); } - os << "]"; - break; - case json_type::object: - os << "{"; - for (auto it = obj_val.begin(); it != obj_val.end(); it++) + return str + "}"; + } + case 7: // array + { + std::string str = "["; + const auto &v = std::get>(value); + for (size_t i = 0; i < v.size(); ++i) { - os << "\"" << it->first << "\":"; - it->second.dump(os); - if (it != --obj_val.end()) - os << ","; + if (i != 0) + str += ","; + str += v[i].dump(); } - os << "}"; - break; + return str + "]"; + } + default: // should never happen + return ""; } } + friend std::ostream &operator<<(std::ostream &os, const json &j) { return os << j.dump(); } + private: - json_type type; - bool bool_val = false; - std::string str_val; - std::map obj_val; - std::vector arr_val; + std::variant>, std::vector> value; }; - JSON_EXPORT json load(std::istream &is); - inline json load(const char *str) - { - std::stringstream ss; - ss << str; - return load(ss); - } - inline json load(const std::string &str) - { - std::stringstream ss; - ss << str; - return load(ss); - } - JSON_EXPORT std::string parse_string(std::istream &is); + /** + * Loads a JSON object from the given input stream. + * + * @param is The input stream to read the JSON object from. + * @return The loaded JSON object. + */ + [[nodiscard]] json load(std::istream &is); + + /** + * Loads a JSON object from a string. + * + * @param str The string containing the JSON object. + * @return The loaded JSON object. + */ + [[nodiscard]] json load(std::string_view str); + + /** + * Validates a JSON value against a JSON schema. + * + * @param value The JSON value to be validated. + * @param schema The JSON schema to validate against. + * @param schema_refs The JSON schema references. + * @return `true` if the value is valid according to the schema, `false` otherwise. + */ + [[nodiscard]] bool validate(const json &value, const json &schema, const json &schema_refs); } // namespace json diff --git a/src/json.cpp b/src/json.cpp index 0b12f19..2ee06a1 100644 --- a/src/json.cpp +++ b/src/json.cpp @@ -1,23 +1,131 @@ #include "json.hpp" -#include - -inline char get_char(std::istream &is) { return static_cast(is.get()); } +#include +#include namespace json { - JSON_EXPORT json load(std::istream &is) + json::json(std::initializer_list init) : value(nullptr) + { + if (init.size() == 2 && init.begin()->is_string()) + { // we have a key-value pair.. + value = std::map>(); + std::get>>(value)[static_cast(*init.begin())] = *(init.begin() + 1); + } + else if (std::all_of(init.begin(), init.end(), [](const json &j) + { return j.is_object() && j.size() == 1; })) + { // we have an array of key-value pairs.. + value = std::map>(); + for (const auto &j : init) + std::get>>(value)[static_cast(j.as_object().begin()->first)] = j.as_object().begin()->second; + } + else + { // we have an array.. + value = std::vector(); + for (const auto &j : init) + std::get>(value).push_back(j); + } + } + + std::string parse_string(std::istream &is) + { + is.get(); + std::string val; + while (is.peek() != '\"') + if (is.peek() == '\\') + { + is.get(); + switch (is.get()) + { + case '\"': + val += '\"'; + break; + case '\\': + val += '\\'; + break; + case '/': + val += '/'; + break; + case 'b': + val += '\b'; + break; + case 'f': + val += '\f'; + break; + case 'n': + val += '\n'; + break; + case 'r': + val += '\r'; + break; + case 't': + val += '\t'; + break; + case 'u': + { + int codepoint = 0; + const auto factors = {12u, 8u, 4u, 0u}; + char c; + for (const auto factor : factors) + { + c = static_cast(is.get()); + if (c >= '0' && c <= '9') + codepoint += static_cast((static_cast(c) - 0x30u) << factor); + else if (c >= 'A' && c <= 'F') + codepoint += static_cast((static_cast(c) - 0x37u) << factor); + else if (c >= 'a' && c <= 'f') + codepoint += static_cast((static_cast(c) - 0x57u) << factor); + else + throw std::invalid_argument("not a valid json"); + } + + // translate codepoint into bytes + if (codepoint < 0x80) + { // 1-byte characters: 0xxxxxxx (ASCII) + val.push_back(static_cast(codepoint)); + } + else if (codepoint <= 0x7FF) + { // 2-byte characters: 110xxxxx 10xxxxxx + val.push_back(static_cast(0xC0u | (static_cast(codepoint) >> 6u))); + val.push_back(static_cast(0x80u | (static_cast(codepoint) & 0x3Fu))); + } + else if (codepoint <= 0xFFFF) + { // 3-byte characters: 1110xxxx 10xxxxxx 10xxxxxx + val.push_back(static_cast(0xE0u | (static_cast(codepoint) >> 12u))); + val.push_back(static_cast(0x80u | ((static_cast(codepoint) >> 6u) & 0x3Fu))); + val.push_back(static_cast(0x80u | (static_cast(codepoint) & 0x3Fu))); + } + else + { // 4-byte characters: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + val.push_back(static_cast(0xF0u | (static_cast(codepoint) >> 18u))); + val.push_back(static_cast(0x80u | ((static_cast(codepoint) >> 12u) & 0x3Fu))); + val.push_back(static_cast(0x80u | ((static_cast(codepoint) >> 6u) & 0x3Fu))); + val.push_back(static_cast(0x80u | (static_cast(codepoint) & 0x3Fu))); + } + break; + } + default: + throw std::invalid_argument("not a valid json"); + } + } + else + val += static_cast(is.get()); + is.get(); + return val; + } + + json load(std::istream &is) { is >> std::ws; // we remove all the leading whitespace.. switch (is.peek()) { case '{': { // we have a json object.. - get_char(is); - std::map vals; + is.get(); + json vals; is >> std::ws; if (is.peek() == '}') { // we have an empty object.. - get_char(is); + is.get(); return vals; } do @@ -27,31 +135,32 @@ namespace json throw std::invalid_argument("not a valid json"); std::string id = parse_string(is); is >> std::ws; - if (get_char(is) != ':') + if (is.get() != ':') throw std::invalid_argument("not a valid json"); auto val = load(is); - vals.emplace(id, std::move(val)); + vals[id] = std::move(val); is >> std::ws; - } while (is.peek() == ',' && get_char(is)); - if (get_char(is) != '}') + } while (is.peek() == ',' && is.get()); + if (is.get() != '}') throw std::invalid_argument("not a valid json"); return vals; } case '[': { // we have a json array.. - get_char(is); - std::vector vals; + is.get(); + json vals(json_type::array); + is >> std::ws; if (is.peek() == ']') { // we have an empty array.. - get_char(is); + is.get(); return vals; } do { - vals.emplace_back(load(is)); + vals.push_back(load(is)); is >> std::ws; - } while (is.peek() == ',' && get_char(is)); - if (get_char(is) != ']') + } while (is.peek() == ',' && is.get()); + if (is.get() != ']') throw std::invalid_argument("not a valid json"); return vals; } @@ -68,85 +177,85 @@ namespace json case '9': { std::string num; - num += get_char(is); + num += static_cast(is.get()); while (is.peek() == '0' || is.peek() == '1' || is.peek() == '2' || is.peek() == '3' || is.peek() == '4' || is.peek() == '5' || is.peek() == '6' || is.peek() == '7' || is.peek() == '8' || is.peek() == '9') - num += get_char(is); + num += static_cast(is.get()); if (is.peek() == '.') { - num += get_char(is); + num += static_cast(is.get()); while (is.peek() == '0' || is.peek() == '1' || is.peek() == '2' || is.peek() == '3' || is.peek() == '4' || is.peek() == '5' || is.peek() == '6' || is.peek() == '7' || is.peek() == '8' || is.peek() == '9') - num += get_char(is); + num += static_cast(is.get()); if (is.peek() == 'e' || is.peek() == 'E') { - num += get_char(is); + num += static_cast(is.get()); if (is.peek() == '+') - num += get_char(is); + num += static_cast(is.get()); if (is.peek() == '-') - num += get_char(is); + num += static_cast(is.get()); while (is.peek() == '0' || is.peek() == '1' || is.peek() == '2' || is.peek() == '3' || is.peek() == '4' || is.peek() == '5' || is.peek() == '6' || is.peek() == '7' || is.peek() == '8' || is.peek() == '9') - num += get_char(is); - return json(num, true); + num += static_cast(is.get()); + return json(std::stod(num)); } - return json(num, true); + return json(std::stod(num)); } else if (is.peek() == 'e' || is.peek() == 'E') { - num += get_char(is); + num += static_cast(is.get()); if (is.peek() == '+') - num += get_char(is); + num += static_cast(is.get()); if (is.peek() == '-') - num += get_char(is); + num += static_cast(is.get()); while (is.peek() == '0' || is.peek() == '1' || is.peek() == '2' || is.peek() == '3' || is.peek() == '4' || is.peek() == '5' || is.peek() == '6' || is.peek() == '7' || is.peek() == '8' || is.peek() == '9') - num += get_char(is); - return json(num, true); + num += static_cast(is.get()); + return json(std::stod(num)); } else - return json(num, true); + return json(static_cast(std::stol(num))); } case '.': { std::string num; - num += get_char(is); + num += static_cast(is.get()); while (is.peek() == '0' || is.peek() == '1' || is.peek() == '2' || is.peek() == '3' || is.peek() == '4' || is.peek() == '5' || is.peek() == '6' || is.peek() == '7' || is.peek() == '8' || is.peek() == '9') - num += get_char(is); + num += static_cast(is.get()); if (is.peek() == 'e' || is.peek() == 'E') { - num += get_char(is); + num += static_cast(is.get()); if (is.peek() == '+') - num += get_char(is); + num += static_cast(is.get()); if (is.peek() == '-') - num += get_char(is); + num += static_cast(is.get()); while (is.peek() == '0' || is.peek() == '1' || is.peek() == '2' || is.peek() == '3' || is.peek() == '4' || is.peek() == '5' || is.peek() == '6' || is.peek() == '7' || is.peek() == '8' || is.peek() == '9') - num += get_char(is); - return json(num, true); + num += static_cast(is.get()); + return json(std::stod(num)); } - return json(num, true); + return json(std::stod(num)); } case 'f': { // we have a false literal.. - get_char(is); - if (get_char(is) == 'a' && get_char(is) == 'l' && get_char(is) == 's' && get_char(is) == 'e') + is.get(); + if (is.get() == 'a' && is.get() == 'l' && is.get() == 's' && is.get() == 'e') return false; throw std::invalid_argument("not a valid json"); } case 't': { // we have a true literal.. - get_char(is); - if (get_char(is) == 'r' && get_char(is) == 'u' && get_char(is) == 'e') + is.get(); + if (is.get() == 'r' && is.get() == 'u' && is.get() == 'e') return true; throw std::invalid_argument("not a valid json"); } case 'n': { // we have a null literal.. - get_char(is); - switch (get_char(is)) + is.get(); + switch (is.get()) { case 'a': - if (get_char(is) == 'n') + if (is.get() == 'n') return nullptr; throw std::invalid_argument("not a valid json"); case 'u': - if (get_char(is) == 'l' && get_char(is) == 'l') + if (is.get() == 'l' && is.get() == 'l') return nullptr; throw std::invalid_argument("not a valid json"); default: @@ -157,21 +266,21 @@ namespace json return parse_string(is); case '/': { // we have a json comment.. - get_char(is); + is.get(); if (is.peek() == '/') { while (is.peek() != '\n') - get_char(is); + is.get(); return load(is); } else if (is.peek() == '*') { while (is.peek() != '*') - get_char(is); - get_char(is); + is.get(); + is.get(); if (is.peek() == '/') { - get_char(is); + is.get(); return load(is); } else @@ -185,90 +294,152 @@ namespace json } } - JSON_EXPORT std::string parse_string(std::istream &is) + json load(std::string_view str) { - get_char(is); - std::string val; - while (is.peek() != '\"') - if (is.peek() == '\\') + std::stringstream ss; + ss << str; + return load(ss); + } + + bool validate(const json &value, const json &schema, const json &schema_refs) + { + if (schema.contains("type")) + { // we have a type.. + if (schema["type"] == "object") { - get_char(is); - switch (get_char(is)) + if (!value.is_object()) + return false; + for (const auto &property : schema["properties"].as_object()) { - case '\"': - val += '\"'; - break; - case '\\': - val += '\\'; - break; - case '/': - val += '/'; - break; - case 'b': - val += '\b'; - break; - case 'f': - val += '\f'; - break; - case 'n': - val += '\n'; - break; - case 'r': - val += '\r'; - break; - case 't': - val += '\t'; - break; - case 'u': + if (!value.contains(property.first)) + return false; + if (!validate(value[property.first], property.second, schema_refs)) + return false; + } + return true; + } + else if (schema["type"] == "array") + { + if (!value.is_array()) + return false; + if (schema.contains("minItems")) { - int codepoint = 0; - const auto factors = {12u, 8u, 4u, 0u}; - char c; - for (const auto factor : factors) - { - c = get_char(is); - if (c >= '0' && c <= '9') - codepoint += static_cast((static_cast(c) - 0x30u) << factor); - else if (c >= 'A' && c <= 'F') - codepoint += static_cast((static_cast(c) - 0x37u) << factor); - else if (c >= 'a' && c <= 'f') - codepoint += static_cast((static_cast(c) - 0x57u) << factor); - else - throw std::invalid_argument("not a valid json"); - } - - // translate codepoint into bytes - if (codepoint < 0x80) - { // 1-byte characters: 0xxxxxxx (ASCII) - val.push_back(static_cast(codepoint)); - } - else if (codepoint <= 0x7FF) - { // 2-byte characters: 110xxxxx 10xxxxxx - val.push_back(static_cast(0xC0u | (static_cast(codepoint) >> 6u))); - val.push_back(static_cast(0x80u | (static_cast(codepoint) & 0x3Fu))); - } - else if (codepoint <= 0xFFFF) - { // 3-byte characters: 1110xxxx 10xxxxxx 10xxxxxx - val.push_back(static_cast(0xE0u | (static_cast(codepoint) >> 12u))); - val.push_back(static_cast(0x80u | ((static_cast(codepoint) >> 6u) & 0x3Fu))); - val.push_back(static_cast(0x80u | (static_cast(codepoint) & 0x3Fu))); - } - else - { // 4-byte characters: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - val.push_back(static_cast(0xF0u | (static_cast(codepoint) >> 18u))); - val.push_back(static_cast(0x80u | ((static_cast(codepoint) >> 12u) & 0x3Fu))); - val.push_back(static_cast(0x80u | ((static_cast(codepoint) >> 6u) & 0x3Fu))); - val.push_back(static_cast(0x80u | (static_cast(codepoint) & 0x3Fu))); - } - break; + uint64_t min = schema["minItems"]; + if (value.size() < min) + return false; } - default: - throw std::invalid_argument("not a valid json"); + if (schema.contains("maxItems")) + { + uint64_t max = schema["maxItems"]; + if (value.size() > max) + return false; + } + for (const auto &v : value.as_array()) + if (!validate(v, schema["items"], schema_refs)) + return false; + return true; + } + else if (schema["type"] == "string") + { + if (!value.is_string()) + return false; + if (schema.contains("enum")) + return std::find(schema["enum"].as_array().begin(), schema["enum"].as_array().end(), value) != schema["enum"].as_array().end(); + return true; + } + else if (schema["type"] == "number") + { + if (!value.is_number()) + return false; + if (schema.contains("minimum")) + { + double min = schema["minimum"]; + double v = value; + if (v < min) + return false; + } + if (schema.contains("maximum")) + { + double max = schema["maximum"]; + double v = value; + if (v > max) + return false; + } + return true; + } + else if (schema["type"] == "integer") + { + if (!value.is_number()) + return false; + if (schema.contains("minimum")) + { + int64_t min = schema["minimum"]; + int64_t v = value; + if (v < min) + return false; } + if (schema.contains("maximum")) + { + int64_t max = schema["maximum"]; + int64_t v = value; + if (v > max) + return false; + } + return true; } + else if (schema["type"] == "boolean") + return value.is_boolean(); + else if (schema["type"] == "null") + return value.is_null(); + else if (schema["type"] == "any") + return true; else - val += get_char(is); - get_char(is); - return val; + return false; + } + else if (schema.contains("$ref")) + { // we have a reference to another schema.. + std::string ref = schema["$ref"]; + size_t pos = 0; + std::string token; + json current = schema_refs; + while ((pos = ref.find('/')) != std::string::npos) + { + token = ref.substr(0, pos); + if (token != "#") + { + if (!current.contains(token)) + return false; + json next = current[token]; + current = next; + } + ref.erase(0, pos + 1); + } + return validate(value, current[ref], schema_refs); + } + else if (schema.contains("allOf")) + { // we have a list of schemas that must all be valid.. + for (const auto &s : schema["allOf"].as_array()) + if (!validate(value, s, schema_refs)) + return false; + return true; + } + else if (schema.contains("anyOf")) + { // we have a list of schemas where at least one must be valid.. + for (const auto &s : schema["anyOf"].as_array()) + if (validate(value, s, schema_refs)) + return true; + return false; + } + else if (schema.contains("oneOf")) + { // we have a list of schemas where exactly one must be valid.. + int count = 0; + for (const auto &s : schema["oneOf"].as_array()) + if (validate(value, s, schema_refs)) + count++; + return count == 1; + } + else if (schema.contains("not")) // we have a schema that must not be valid.. + return !validate(value, schema["not"], schema_refs); + return false; } -} // namespace json +} // namespace json \ No newline at end of file diff --git a/src/jsonConfig.cmake.in b/src/jsonConfig.cmake.in deleted file mode 100644 index 12d6f58..0000000 --- a/src/jsonConfig.cmake.in +++ /dev/null @@ -1,7 +0,0 @@ -set(JSON_VERSION 0.1.0) - -@PACKAGE_INIT@ - -set_and_check(JSON_INCLUDE_DIR "@PACKAGE_CMAKE_INSTALL_INCLUDEDIR@/json") - -check_required_components(json) \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 78225ce..c74e5d3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,5 +1,6 @@ add_executable(json_lib_tests test_json.cpp) add_dependencies(json_lib_tests json) target_link_libraries(json_lib_tests PRIVATE json) +setup_sanitizers(json_lib_tests) add_test(NAME JSON_LibTest COMMAND json_lib_tests WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) \ No newline at end of file diff --git a/tests/test_json.cpp b/tests/test_json.cpp index d4b8fd8..77a7083 100644 --- a/tests/test_json.cpp +++ b/tests/test_json.cpp @@ -1,8 +1,168 @@ #include "json.hpp" -#include "logging.h" +#include "logging.hpp" #include #include +void test_constructors() +{ + json::json j0; + assert(j0.get_type() == json::json_type::object); + assert(j0.is_object()); + assert(j0.size() == 0); + + json::json j1(1); + assert(j1.get_type() == json::json_type::number); + assert(j1.is_number()); + assert(j1.is_integer()); + assert(!j1.is_float()); + assert(j1 == 1); + assert(j1.size() == 0); + + json::json j2(2.0); + assert(j2.get_type() == json::json_type::number); + assert(j2.is_number()); + assert(!j2.is_integer()); + assert(j2.is_float()); + assert(j2 == 2.0); + assert(j2.size() == 0); + + json::json j3("3"); + assert(j3.get_type() == json::json_type::string); + assert(j3.is_string()); + assert(j3 == "3"); + assert(j3.size() == 0); + + json::json j4(true); + assert(j4.get_type() == json::json_type::boolean); + assert(j4.is_boolean()); + assert(j4 == true); + assert(j4.size() == 0); + + json::json j5(nullptr); + assert(j5.get_type() == json::json_type::null); + assert(j5.is_null()); + assert(j5 == nullptr); + assert(j5.size() == 0); + + json::json j6(json::json_type::array); + assert(j6.get_type() == json::json_type::array); + assert(j6.is_array()); + assert(j6.size() == 0); + + json::json j7(json::json_type::object); + assert(j7.get_type() == json::json_type::object); + assert(j7.is_object()); + assert(j7.size() == 0); +} + +void test_constructors2() +{ + json::json j0(json::json_type::null); + assert(j0.get_type() == json::json_type::null); + assert(j0.is_null()); + assert(j0 == nullptr); + assert(j0.size() == 0); + + json::json j1(json::json_type::array); + assert(j1.get_type() == json::json_type::array); + assert(j1.is_array()); + assert(j1.size() == 0); + + json::json j2(json::json_type::object); + assert(j2.get_type() == json::json_type::object); + assert(j2.is_object()); + assert(j2.size() == 0); + + json::json j3(json::json_type::number); + assert(j3.get_type() == json::json_type::number); + assert(j3.is_number()); + assert(j3.is_integer() || j3.is_float()); + assert(j3 == 0); + assert(j3.size() == 0); + + json::json j4(json::json_type::string); + assert(j4.get_type() == json::json_type::string); + assert(j4.is_string()); + assert(j4 == ""); + assert(j4.size() == 0); + + json::json j5(json::json_type::boolean); + assert(j5.get_type() == json::json_type::boolean); + assert(j5.is_boolean()); + assert(j5 == false); + assert(j5.size() == 0); + + json::json j6(json::json_type::array); + j6.push_back(1); + j6.push_back(2); + j6.push_back(3); + assert(j6.get_type() == json::json_type::array); + assert(j6.is_array()); + assert(j6.size() == 3); + + json::json j7(std::move(j6)); + assert(j6.get_type() == json::json_type::null); + assert(j6.is_null()); + assert(j7.get_type() == json::json_type::array); + assert(j7.is_array()); + assert(j7.size() == 3); + assert(j7[0] == 1); + assert(j7[1] == 2); + assert(j7[2] == 3); +} + +void test_assignments() +{ + json::json j0; + assert(j0.get_type() == json::json_type::object); + assert(j0 == json::json{}); + assert(j0.size() == 0); + + json::json j1 = 1; + assert(j1.get_type() == json::json_type::number); + assert(j1.is_number()); + assert(j1.is_integer()); + assert(!j1.is_float()); + assert(j1 == 1); + assert(j1.size() == 0); + + json::json j2 = 2.0; + assert(j2.get_type() == json::json_type::number); + assert(j2.is_number()); + assert(!j2.is_integer()); + assert(j2.is_float()); + assert(j2 == 2.0); + assert(j2.size() == 0); + + json::json j3 = "3"; + assert(j3.get_type() == json::json_type::string); + assert(j3.is_string()); + assert(j3 == "3"); + assert(j3.size() == 0); + + json::json j4 = true; + assert(j4.get_type() == json::json_type::boolean); + assert(j4.is_boolean()); + assert(j4 == true); + assert(j4.size() == 0); + + json::json j5 = nullptr; + assert(j5.get_type() == json::json_type::null); + assert(j5.is_null()); + assert(j5 == nullptr); + assert(j5.size() == 0); + + json::json j6 = json::json_type::array; + assert(j6.get_type() == json::json_type::array); + assert(j6.is_array()); + assert(j6.size() == 0); + + json::json j7 = json::json_type::object; + assert(j7.get_type() == json::json_type::object); + assert(j7.is_object()); + assert(j7.size() == 0); +} + void test_json() { std::stringstream ss; @@ -56,6 +216,7 @@ void test_json_escapes() } )"; json::json j = json::load(ss); + assert(j["choices"][0]["message"]["function_call"]["arguments"].dump() == "\"{\\n \\\"purpose\\\": \\\"attività personalizzate\\\"\\n}\""); } void test_json_special_chars() @@ -67,7 +228,7 @@ void test_json_special_chars() } )"; json::json j = json::load(ss); - LOG(j); + LOG_INFO(j); assert(j["a"] == "\b\f\n\r\t\"\\"); } @@ -81,7 +242,7 @@ void test_initializer_lists() {"e", nullptr}, {"f", {1, 2}}, {"g", {{"h", 1}, {"i", 2}}}}; - LOG(j); + LOG_INFO(j); assert(j["a"] == 1); assert(j["b"] == 2.0); assert(j["c"] == "3"); @@ -95,8 +256,8 @@ void test_initializer_lists() void test_json_comparison() { - json::json j0 = nullptr; - json::json j1 = nullptr; + json::json j0; + json::json j1; assert(j0 == j1); j0["a"] = 1; @@ -113,9 +274,11 @@ void test_json_comparison() assert(j0 == j1); + j0["f"] = json::json(json::json_type::array); j0["f"].push_back(1); j0["f"].push_back(2); + j1["f"] = json::json(json::json_type::array); j1["f"].push_back(1); j1["f"].push_back(2); @@ -127,12 +290,13 @@ void test_json_comparison() void test_move_semantics() { - json::json j0 = nullptr; + json::json j0; j0["a"] = 1; j0["b"] = 2.0; j0["c"] = "3"; j0["d"] = true; j0["e"] = nullptr; + j0["f"] = json::json(json::json_type::array); j0["f"].push_back(1); j0["f"].push_back(2); j0["f"].push_back(3); @@ -152,12 +316,12 @@ void test_move_semantics() void test_move_into_array() { - json::json j0; + json::json j0 = json::json_type::array; j0.push_back(1); j0.push_back(2); j0.push_back(3); - json::json j1; + json::json j1 = json::json_type::array; j1.push_back(std::move(j0)); assert(j0 == nullptr); @@ -169,27 +333,28 @@ void test_move_into_array() void test_iterate() { - json::json j0 = nullptr; + json::json j0; j0["a"] = 1; j0["b"] = 2.0; j0["c"] = "3"; j0["d"] = true; j0["e"] = nullptr; + j0["f"] = json::json(json::json_type::array); j0["f"].push_back(1); j0["f"].push_back(2); j0["f"].push_back(3); - std::map &m = j0.get_object(); + auto &m = j0.as_object(); for ([[maybe_unused]] auto &[key, value] : m) - LOG("key " << key << " value " << value); + LOG_INFO("key " << key << " value " << value); - json::json j1 = nullptr; + json::json j1 = json::json_type::array; j1.push_back(1); j1.push_back(2); j1.push_back(3); - for ([[maybe_unused]] auto &value : j1.get_array()) - LOG("value " << value); + for ([[maybe_unused]] auto &value : j1.as_array()) + LOG_INFO("value " << value); } void test_null() @@ -197,36 +362,48 @@ void test_null() json::json j0 = nullptr; assert(j0 == nullptr); assert(j0.get_type() == json::json_type::null); - assert(j0.to_string() == "null"); + assert(j0.is_null()); + assert(j0.dump() == "null"); } void test_empty_array() { json::json j0 = json::json_type::array; assert(j0.get_type() == json::json_type::array); - assert(j0.to_string() == "[]"); + assert(j0.is_array()); + assert(j0.dump() == "[]"); } void test_empty_object() { json::json j0 = json::json_type::object; assert(j0.get_type() == json::json_type::object); - assert(j0.to_string() == "{}"); + assert(j0.is_object()); + assert(j0.dump() == "{}"); } void test_scientific_numbers() { json::json j0 = 1e+10; assert(j0.get_type() == json::json_type::number); - assert(j0.to_string() == std::to_string(1e+10)); + assert(j0.is_number()); + assert(!j0.is_integer()); + assert(j0.is_float()); + assert(j0.dump() == std::to_string(1e+10)); json::json j1 = 1.23e+10; assert(j1.get_type() == json::json_type::number); - assert(j1.to_string() == std::to_string(1.23e+10)); + assert(j1.is_number()); + assert(!j1.is_integer()); + assert(j1.is_float()); + assert(j1.dump() == std::to_string(1.23e+10)); json::json j2 = .23e+10; assert(j2.get_type() == json::json_type::number); - assert(j2.to_string() == std::to_string(.23e+10)); + assert(j2.is_number()); + assert(!j2.is_integer()); + assert(j2.is_float()); + assert(j2.dump() == std::to_string(.23e+10)); } void test_array_of_scientific_numbers() @@ -236,11 +413,128 @@ void test_array_of_scientific_numbers() j0.push_back(1.23e+10); j0.push_back(.23e+10); assert(j0.get_type() == json::json_type::array); - assert(j0.to_string() == "[" + std::to_string(1e+10) + "," + std::to_string(1.23e+10) + "," + std::to_string(.23e+10) + "]"); + assert(j0[0].is_number()); + assert(!j0[0].is_integer()); + assert(j0[0].is_float()); + assert(j0[1].is_number()); + assert(!j0[1].is_integer()); + assert(j0[1].is_float()); + assert(j0[2].is_number()); + assert(!j0[2].is_integer()); + assert(j0[2].is_float()); + assert(j0.dump() == "[" + std::to_string(1e+10) + "," + std::to_string(1.23e+10) + "," + std::to_string(.23e+10) + "]"); } -int main(int, char **) +void test_validate() { + // Test case 1: Validating an object against a schema with properties + json::json value1 = { + {"name", "John"}, + {"age", 30}, + {"city", "New York"}}; + json::json schema1 = { + {"type", "object"}, + {"properties", {{"name", {{"type", "string"}}}, {"age", {{"type", "number"}}}, {"city", {{"type", "string"}}}}}}; + json::json schemaRefs1 = {}; + assert(json::validate(value1, schema1, schemaRefs1)); + + // Test case 2: Validating an array against a schema with items + json::json value2 = {1, 2, 3, 4, 5}; + json::json schema2 = { + {"type", "array"}, + {"items", {{"type", "number"}}}}; + json::json schemaRefs2 = {}; + assert(json::validate(value2, schema2, schemaRefs2)); + + // Test case 3: Validating a string against a schema with enum values + json::json value3 = "apple"; + json::json schema3 = { + {"type", "string"}, + {"enum", {"apple", "banana", "orange"}}}; + json::json schemaRefs3 = {}; + assert(json::validate(value3, schema3, schemaRefs3)); + + // Test case 4: Validating a number against a schema with minimum and maximum values + json::json value4 = 10.5; + json::json schema4 = { + {"type", "number"}, + {"minimum", 0}, + {"maximum", 20}}; + json::json schemaRefs4 = {}; + assert(json::validate(value4, schema4, schemaRefs4)); + + // Test case 5: Validating a boolean against a schema with type boolean + json::json value5 = true; + json::json schema5 = { + {"type", "boolean"}}; + json::json schemaRefs5 = {}; + assert(json::validate(value5, schema5, schemaRefs5)); + + // Test case 6: Validating null against a schema with type null + json::json value6 = nullptr; + json::json schema6 = { + {"type", "null"}}; + json::json schemaRefs6 = {}; + assert(json::validate(value6, schema6, schemaRefs6)); + + // Test case 7: Validating an object against a schema with a reference + json::json value7 = { + {"name", "John"}, + {"age", 30}, + {"city", "New York"}}; + json::json schema7 = { + {"$ref", "#/definitions/person"}}; + json::json schemaRefs7 = { + {"definitions", {{"person", {{"type", "object"}, {"properties", {{"name", {{"type", "string"}}}, {"age", {{"type", "number"}}}, {"city", {{"type", "string"}}}}}}}}}}; + assert(json::validate(value7, schema7, schemaRefs7)); + + // Test case 8: Validating an object against a schema with allOf + json::json value8 = { + {"name", "John"}, + {"age", 30}, + {"city", "New York"}}; + json::json schema8 = { + {"allOf", std::vector{{{"type", "object"}, {"properties", {{"name", {{"type", "string"}}}, {"age", {{"type", "number"}}}, {"city", {{"type", "string"}}}}}}}}}; + json::json schemaRefs8 = {}; + assert(json::validate(value8, schema8, schemaRefs8)); + + // Test case 9: Validating an object against a schema with anyOf + json::json value9 = { + {"name", "John"}, + {"age", 30}, + {"city", "New York"}}; + json::json schema9 = { + {"anyOf", std::vector{{{"type", "object"}, {"properties", {{"name", {{"type", "string"}}}, {"age", {{"type", "number"}}}, {"city", {{"type", "string"}}}}}}}}}; + json::json schemaRefs9 = {}; + assert(json::validate(value9, schema9, schemaRefs9)); + + // Test case 10: Validating an object against a schema with oneOf + json::json value10 = { + {"name", "John"}, + {"age", 30}, + {"city", "New York"}}; + json::json schema10 = { + {"oneOf", std::vector{{{"type", "object"}, {"properties", {{"name", {{"type", "string"}}}, {"age", {{"type", "number"}}}, {"city", {{"type", "string"}}}}}}}}}; + json::json schemaRefs10 = {}; + assert(json::validate(value10, schema10, schemaRefs10)); + + // Test case 11: Validating an object against a schema with not + json::json value11 = { + {"name", "John"}, + {"age", 30}, + {"city", "New York"}}; + json::json schema11 = { + {"not", {{"type", "string"}}}}; + json::json schemaRefs11 = {}; + assert(json::validate(value11, schema11, schemaRefs11)); +} + +int main() +{ + test_constructors(); + test_constructors2(); + test_assignments(); + test_json(); test_json_escapes(); test_json_special_chars(); @@ -258,5 +552,7 @@ int main(int, char **) test_scientific_numbers(); test_array_of_scientific_numbers(); + test_validate(); + return 0; } \ No newline at end of file