feat(bindings): add modern C++ interface for iKv
This commit is contained in:
56
.gitea/workflows/bindings.yml
Normal file
56
.gitea/workflows/bindings.yml
Normal file
@@ -0,0 +1,56 @@
|
||||
name: C++ bindings
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: ${{ matrix.compiler }} / ${{ matrix.build_type }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- compiler: gcc
|
||||
cc: gcc
|
||||
cxx: g++
|
||||
build_type: Debug
|
||||
- compiler: gcc
|
||||
cc: gcc
|
||||
cxx: g++
|
||||
build_type: Release
|
||||
- compiler: clang
|
||||
cc: clang
|
||||
cxx: clang++
|
||||
build_type: Debug
|
||||
- compiler: clang
|
||||
cc: clang
|
||||
cxx: clang++
|
||||
build_type: Release
|
||||
|
||||
steps:
|
||||
- name: Checkout with submodules
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Configure
|
||||
env:
|
||||
CC: ${{ matrix.cc }}
|
||||
CXX: ${{ matrix.cxx }}
|
||||
run: >-
|
||||
cmake -S . -B build
|
||||
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
|
||||
-DIKVXX_BUILD_TESTS=ON
|
||||
-DIKVXX_INSTALL=ON
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build --parallel 2
|
||||
|
||||
- name: Test bindings
|
||||
run: ctest --test-dir build --build-config ${{ matrix.build_type }} --output-on-failure
|
||||
|
||||
- name: Verify installation
|
||||
run: cmake --install build --prefix "${{ runner.temp }}/ikvxx-install"
|
||||
132
.gitignore
vendored
132
.gitignore
vendored
@@ -1,34 +1,110 @@
|
||||
# ---> C++
|
||||
# Prerequisites
|
||||
*.d
|
||||
# Build and installation trees
|
||||
/build/
|
||||
/build-*/
|
||||
/cmake-build-*/
|
||||
/out/
|
||||
/dist/
|
||||
/install/
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
# CMake-generated files from accidental in-source builds
|
||||
/CMakeCache.txt
|
||||
/CMakeFiles/
|
||||
/CTestTestfile.cmake
|
||||
/DartConfiguration.tcl
|
||||
/Makefile
|
||||
/Testing/
|
||||
/_deps/
|
||||
/cmake_install.cmake
|
||||
/compile_commands.json
|
||||
/install_manifest.txt
|
||||
|
||||
# Ninja and Make dependency data
|
||||
.ninja_deps
|
||||
.ninja_log
|
||||
build.ninja
|
||||
rules.ninja
|
||||
*.d
|
||||
*.dep
|
||||
|
||||
# Compiler and linker output
|
||||
*.a
|
||||
*.app
|
||||
*.dll
|
||||
*.dylib
|
||||
*.exe
|
||||
*.exp
|
||||
*.gch
|
||||
*.idb
|
||||
*.ilk
|
||||
*.lai
|
||||
*.lib
|
||||
*.lo
|
||||
*.mod
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
*.smod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
*.lib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.pch
|
||||
*.pdb
|
||||
*.slo
|
||||
*.smod
|
||||
*.so
|
||||
*.so.*
|
||||
|
||||
# Test, benchmark, coverage, and profiling output
|
||||
/ikvxx-test-*
|
||||
*.gcda
|
||||
*.gcno
|
||||
*.gcov
|
||||
*.profdata
|
||||
*.profraw
|
||||
*.trace
|
||||
coverage/
|
||||
coverage.xml
|
||||
lcov.info
|
||||
|
||||
# Package-manager output
|
||||
/.cache/
|
||||
/.conan/
|
||||
/vcpkg_installed/
|
||||
conan.lock
|
||||
conanbuildinfo.*
|
||||
|
||||
# IDE and editor metadata
|
||||
/.idea/
|
||||
/.vs/
|
||||
/.vscode/
|
||||
/.fleet/
|
||||
*.code-workspace
|
||||
*.sln
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.vcxproj
|
||||
*.vcxproj.filters
|
||||
*.vcxproj.user
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Local CMake presets and environment files
|
||||
CMakeUserPresets.json
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Logs and crash dumps
|
||||
*.core
|
||||
*.dmp
|
||||
*.log
|
||||
|
||||
# Operating-system metadata
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
Icon?
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
.Spotlight-V100/
|
||||
.Trashes/
|
||||
._*
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "third_party/iKv"]
|
||||
path = third_party/iKv
|
||||
url = https://dock-it.dev/iDENTITY-Technology/iKv.git
|
||||
52
CMakeLists.txt
Normal file
52
CMakeLists.txt
Normal file
@@ -0,0 +1,52 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(ikvxx VERSION 0.1.0 LANGUAGES C CXX)
|
||||
|
||||
option(IKVXX_BUILD_TESTS "Build iKvxx unit tests" ON)
|
||||
option(IKVXX_INSTALL "Install iKvxx" ON)
|
||||
|
||||
set(IKV_BUILD_DEMOS OFF CACHE BOOL "" FORCE)
|
||||
set(IKV_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(IKV_INSTALL OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(third_party/iKv)
|
||||
set_target_properties(ikv PROPERTIES
|
||||
PUBLIC_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/third_party/iKv/include/ikv.h")
|
||||
|
||||
add_library(ikvxx src/value.cpp)
|
||||
add_library(ikvxx::ikvxx ALIAS ikvxx)
|
||||
target_compile_features(ikvxx PUBLIC cxx_std_17)
|
||||
target_include_directories(ikvxx PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:include>)
|
||||
target_link_libraries(ikvxx PUBLIC ikv::ikv)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(ikvxx PRIVATE /W4 /permissive-)
|
||||
else()
|
||||
target_compile_options(ikvxx PRIVATE -Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
|
||||
if(IKVXX_BUILD_TESTS)
|
||||
include(CTest)
|
||||
add_executable(ikvxx_tests tests/value_tests.cpp)
|
||||
target_link_libraries(ikvxx_tests PRIVATE ikvxx::ikvxx)
|
||||
target_compile_features(ikvxx_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME ikvxx.value COMMAND ikvxx_tests)
|
||||
endif()
|
||||
|
||||
if(IKVXX_INSTALL)
|
||||
include(GNUInstallDirs)
|
||||
include(CMakePackageConfigHelpers)
|
||||
install(TARGETS ikv ikvxx EXPORT ikvxxTargets)
|
||||
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
install(EXPORT ikvxxTargets NAMESPACE ikvxx:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ikvxx)
|
||||
configure_package_config_file(cmake/ikvxxConfig.cmake.in
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/ikvxxConfig.cmake"
|
||||
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ikvxx)
|
||||
write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/ikvxxConfigVersion.cmake"
|
||||
VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion)
|
||||
install(FILES
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/ikvxxConfig.cmake"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/ikvxxConfigVersion.cmake"
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ikvxx)
|
||||
endif()
|
||||
309
README.md
309
README.md
@@ -1,3 +1,310 @@
|
||||
# iKvxx
|
||||
|
||||
C++ bindings for iKv files
|
||||
iKvxx is a modern C++17 binding for [iKv](https://dock-it.dev/iDENTITY-Technology/iKv).
|
||||
It provides automatic resource management, exceptions, type-safe accessors, and a
|
||||
compact `Value` API modeled after JsonCpp while preserving iKv text and binary format
|
||||
compatibility.
|
||||
|
||||
## What it does
|
||||
|
||||
- Parse iKv text from strings or files
|
||||
- Parse iKv binary data from memory or files
|
||||
- Build object and array trees with a JsonCpp-style API
|
||||
- Read and write both `iKv1` and `iKv2`
|
||||
- Write the current `iKv2` format by default
|
||||
- Manage the underlying C tree automatically with RAII
|
||||
- Report parse, type, bounds, allocation, and I/O failures with exceptions
|
||||
- Provide a reusable `ikvxx::ikvxx` CMake target and installable package
|
||||
|
||||
## Syntax preview
|
||||
|
||||
```ikv
|
||||
ikv2 "player_save"
|
||||
{
|
||||
"title" "iKvxx demo"
|
||||
"version" 2
|
||||
"player" {
|
||||
"name" "jondoe"
|
||||
"alive" true
|
||||
"speed" 12.5
|
||||
}
|
||||
"inventory" [
|
||||
"wrench"
|
||||
"battery"
|
||||
"map"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Library example
|
||||
|
||||
```cpp
|
||||
#include <ikvxx/ikvxx.hpp>
|
||||
|
||||
int main()
|
||||
{
|
||||
ikv::Value root(ikv::objectValue, "player_save");
|
||||
root["title"] = "iKvxx demo";
|
||||
root["version"] = 2;
|
||||
|
||||
auto player = root.makeObject("player");
|
||||
player["name"] = "jondoe";
|
||||
player["alive"] = true;
|
||||
player["speed"] = 12.5;
|
||||
|
||||
auto inventory = root.makeArray("inventory", ikv::stringValue);
|
||||
inventory.append("wrench");
|
||||
inventory.append("battery");
|
||||
inventory.append("map");
|
||||
|
||||
root.write("player_save.ikv");
|
||||
}
|
||||
```
|
||||
|
||||
The root and all child handles release their underlying iKv tree automatically. No
|
||||
manual `ikv_free` call is required.
|
||||
|
||||
## Reading values
|
||||
|
||||
Use `load` for text files and the familiar `operator[]` syntax for object and array
|
||||
access:
|
||||
|
||||
```cpp
|
||||
#include <ikvxx/ikvxx.hpp>
|
||||
#include <iostream>
|
||||
|
||||
int main()
|
||||
{
|
||||
auto root = ikv::Value::load("player_save.ikv");
|
||||
|
||||
std::cout << root["title"].asString() << '\n';
|
||||
std::cout << root["version"].asInt() << '\n';
|
||||
std::cout << root["player"]["speed"].asDouble() << '\n';
|
||||
|
||||
auto inventory = root["inventory"];
|
||||
for (ikv::Value::ArrayIndex i = 0; i < inventory.size(); ++i)
|
||||
std::cout << inventory[i].asString() << '\n';
|
||||
}
|
||||
```
|
||||
|
||||
Missing object members produce a null `Value`. Invalid conversions and out-of-range
|
||||
array indexes throw a standard exception:
|
||||
|
||||
```cpp
|
||||
if (!root.isMember("difficulty"))
|
||||
root["difficulty"] = "normal";
|
||||
|
||||
if (root["optional"].isNull())
|
||||
root["optional"] = false;
|
||||
|
||||
try
|
||||
{
|
||||
int value = root["title"].asInt();
|
||||
}
|
||||
catch (const ikv::Error& error)
|
||||
{
|
||||
// "title" is a string, not an integer.
|
||||
}
|
||||
```
|
||||
|
||||
## Parsing text
|
||||
|
||||
`Value::parse` auto-detects `iKv1` and `iKv2` input:
|
||||
|
||||
```cpp
|
||||
const std::string text = R"(
|
||||
ikv2 "settings"
|
||||
{
|
||||
"fullscreen" true
|
||||
"width" 1920
|
||||
"height" 1080
|
||||
}
|
||||
)";
|
||||
|
||||
auto settings = ikv::Value::parse(text);
|
||||
bool fullscreen = settings["fullscreen"].asBool();
|
||||
```
|
||||
|
||||
## Arrays
|
||||
|
||||
Arrays may be untyped or restricted to one iKv value type. Typed arrays reject values
|
||||
of the wrong type:
|
||||
|
||||
```cpp
|
||||
ikv::Value root;
|
||||
|
||||
auto scores = root.makeArray("scores", ikv::intValue);
|
||||
scores.append(10);
|
||||
scores.append(25);
|
||||
scores.append(100);
|
||||
|
||||
auto mixed = root.makeArray("mixed");
|
||||
mixed.append("ready");
|
||||
mixed.append(42);
|
||||
mixed.append(true);
|
||||
|
||||
auto players = root.makeArray("players", ikv::objectValue);
|
||||
auto first = players.appendObject();
|
||||
first["name"] = "Ada";
|
||||
first["score"] = 100;
|
||||
```
|
||||
|
||||
## Binary data
|
||||
|
||||
Binary iKv can be written to a file or encoded directly into a byte vector:
|
||||
|
||||
```cpp
|
||||
ikv::Value root;
|
||||
root["answer"] = 42;
|
||||
|
||||
root.writeBinary("data.ikvb");
|
||||
auto from_file = ikv::Value::loadBinary("data.ikvb");
|
||||
|
||||
std::vector<std::uint8_t> bytes = root.toBinary();
|
||||
auto from_memory = ikv::Value::fromBinary(bytes.data(), bytes.size());
|
||||
```
|
||||
|
||||
Pass an explicit version when compatibility with older data is required:
|
||||
|
||||
```cpp
|
||||
root.write("legacy.ikv", ikv::Version::v1);
|
||||
root.writeBinary("legacy.ikvb", ikv::Version::v1);
|
||||
auto legacy_bytes = root.toBinary(ikv::Version::v1);
|
||||
```
|
||||
|
||||
## Refreshing a tree
|
||||
|
||||
An existing root can be updated in place from a text or binary file. Other `Value`
|
||||
handles that share that root observe the refreshed data:
|
||||
|
||||
```cpp
|
||||
auto settings = ikv::Value::load("settings.ikv");
|
||||
auto shared = settings;
|
||||
|
||||
settings.refresh("updated-settings.ikv");
|
||||
// shared now refers to the refreshed tree as well.
|
||||
```
|
||||
|
||||
`refresh` is only valid on a mutable root value. If refresh fails, the existing tree
|
||||
is preserved.
|
||||
|
||||
## Build
|
||||
|
||||
Clone with the iKv submodule and build with CMake:
|
||||
|
||||
```sh
|
||||
git clone --recurse-submodules https://dock-it.dev/iDENTITY-Technology/iKvxx.git
|
||||
cd iKvxx
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
If the repository was cloned without submodules:
|
||||
|
||||
```sh
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
This produces the reusable static library targets:
|
||||
|
||||
- `ikvxx`
|
||||
- `ikvxx::ikvxx` for CMake consumers
|
||||
|
||||
### CMake consumer usage
|
||||
|
||||
When iKvxx is included directly in another source tree:
|
||||
|
||||
```cmake
|
||||
add_subdirectory(path/to/iKvxx)
|
||||
target_link_libraries(your_target PRIVATE ikvxx::ikvxx)
|
||||
```
|
||||
|
||||
The library can also be installed and consumed with `find_package`:
|
||||
|
||||
```sh
|
||||
cmake --install build --prefix /path/to/ikvxx-install
|
||||
```
|
||||
|
||||
```cmake
|
||||
find_package(ikvxx 0.1 REQUIRED CONFIG)
|
||||
target_link_libraries(your_target PRIVATE ikvxx::ikvxx)
|
||||
```
|
||||
|
||||
Point `CMAKE_PREFIX_PATH` at the selected installation prefix when it is not in a
|
||||
standard system location.
|
||||
|
||||
## Tests
|
||||
|
||||
Build and run the binding tests with CTest:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -DIKVXX_BUILD_TESTS=ON
|
||||
cmake --build build
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
The test suite covers construction, every value type, object and array access,
|
||||
assignment and append overloads, conversion failures, bounds checks, text and binary
|
||||
I/O, format versions, refresh behavior, and shared ownership. Gitea Actions runs the
|
||||
suite on Ubuntu with GCC and Clang in both Debug and Release configurations.
|
||||
|
||||
On Windows, the complete configure, build, and test sequence can be run with:
|
||||
|
||||
```bat
|
||||
tests\run.bat
|
||||
tests\run.bat Debug
|
||||
```
|
||||
|
||||
## Project layout
|
||||
|
||||
- `include/ikvxx/ikvxx.hpp`: public C++ API
|
||||
- `src/value.cpp`: C++ binding implementation
|
||||
- `tests/value_tests.cpp`: unit and integration tests
|
||||
- `third_party/iKv`: pinned iKv Git submodule
|
||||
- `cmake/ikvxxConfig.cmake.in`: installed CMake package configuration
|
||||
- `.gitea/workflows/bindings.yml`: Ubuntu compiler and build matrix
|
||||
|
||||
## Format behavior
|
||||
|
||||
- Generic parse APIs auto-detect `iKv1` and `iKv2`
|
||||
- Generic write APIs default to `iKv2`
|
||||
- Text and binary files remain compatible with the underlying iKv C library
|
||||
- Explicit `ikv::Version::v1` and `ikv::Version::v2` output is supported
|
||||
- `iKv2` binary input retains iKv's indexed, lazy root lookup behavior
|
||||
|
||||
## Public API
|
||||
|
||||
The public interface is declared in
|
||||
[`include/ikvxx/ikvxx.hpp`](./include/ikvxx/ikvxx.hpp). Key areas include:
|
||||
|
||||
- `ikv::Value` construction and automatic lifetime management
|
||||
- JsonCpp-style object lookup with `operator[]`
|
||||
- Array indexing and append operations
|
||||
- Runtime type inspection with `isString`, `isInt`, `isObject`, and related methods
|
||||
- Typed scalar access with `asString`, `asInt`, `asInt64`, `asDouble`, and `asBool`
|
||||
- Text parsing, loading, writing, and in-place refresh
|
||||
- Binary memory and file I/O
|
||||
- Explicit format-version selection
|
||||
|
||||
## API constraints
|
||||
|
||||
iKvxx intentionally exposes errors where the underlying iKv model cannot safely
|
||||
provide JsonCpp behavior:
|
||||
|
||||
- Roots must be objects or arrays; scalar roots are not supported by iKv
|
||||
- Arrays of arrays are not supported by the current iKv C API
|
||||
- Existing array elements cannot be replaced in place
|
||||
- Copied `Value` handles share ownership of the same tree
|
||||
- Structural assignment between two `Value` objects is disabled because the public C
|
||||
API cannot clone arbitrary subtrees
|
||||
- A child handle must not be retained across replacement of that child or a root
|
||||
refresh; acquire the child again after modifying the structure
|
||||
|
||||
## License
|
||||
|
||||
This repository ships with the license text in [`LICENSE`](./LICENSE):
|
||||
|
||||
- Creative Commons Attribution-ShareAlike 4.0 International (`CC BY-SA 4.0`)
|
||||
|
||||
The iKv submodule includes its own license file at
|
||||
[`third_party/iKv/LICENSE`](./third_party/iKv/LICENSE).
|
||||
|
||||
5
cmake/ikvxxConfig.cmake.in
Normal file
5
cmake/ikvxxConfig.cmake.in
Normal file
@@ -0,0 +1,5 @@
|
||||
@PACKAGE_INIT@
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/ikvxxTargets.cmake")
|
||||
|
||||
check_required_components(ikvxx)
|
||||
113
include/ikvxx/ikvxx.hpp
Normal file
113
include/ikvxx/ikvxx.hpp
Normal file
@@ -0,0 +1,113 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct ikv_node_t;
|
||||
|
||||
namespace ikv {
|
||||
|
||||
enum ValueType {
|
||||
nullValue = 0,
|
||||
stringValue,
|
||||
intValue,
|
||||
realValue,
|
||||
booleanValue,
|
||||
objectValue,
|
||||
arrayValue
|
||||
};
|
||||
|
||||
enum class Version : unsigned { v1 = 1, v2 = 2 };
|
||||
|
||||
class Error : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
class Value {
|
||||
public:
|
||||
using ArrayIndex = std::uint32_t;
|
||||
|
||||
explicit Value(ValueType type = objectValue, std::string root_name = "root");
|
||||
Value(const Value&) = default;
|
||||
Value& operator=(const Value&) = delete;
|
||||
|
||||
static Value parse(const std::string& text);
|
||||
static Value load(const std::string& path);
|
||||
static Value fromBinary(const void* data, std::size_t size);
|
||||
static Value loadBinary(const std::string& path);
|
||||
|
||||
ValueType type() const noexcept;
|
||||
bool isNull() const noexcept;
|
||||
bool isString() const noexcept;
|
||||
bool isInt() const noexcept;
|
||||
bool isDouble() const noexcept;
|
||||
bool isBool() const noexcept;
|
||||
bool isObject() const noexcept;
|
||||
bool isArray() const noexcept;
|
||||
bool empty() const;
|
||||
std::size_t size() const;
|
||||
std::string name() const;
|
||||
|
||||
bool isMember(const std::string& key) const;
|
||||
Value operator[](const std::string& key);
|
||||
Value operator[](const char* key);
|
||||
Value operator[](const std::string& key) const;
|
||||
Value operator[](const char* key) const;
|
||||
Value operator[](ArrayIndex index);
|
||||
Value operator[](int index);
|
||||
Value operator[](ArrayIndex index) const;
|
||||
Value operator[](int index) const;
|
||||
Value get(const std::string& key, const Value& fallback) const;
|
||||
|
||||
Value& operator=(const std::string& value);
|
||||
Value& operator=(const char* value);
|
||||
Value& operator=(std::int64_t value);
|
||||
Value& operator=(int value);
|
||||
Value& operator=(double value);
|
||||
Value& operator=(bool value);
|
||||
|
||||
Value makeObject(const std::string& key);
|
||||
Value makeArray(const std::string& key, ValueType element_type = nullValue);
|
||||
Value append(const std::string& value);
|
||||
Value append(const char* value);
|
||||
Value append(std::int64_t value);
|
||||
Value append(int value);
|
||||
Value append(double value);
|
||||
Value append(bool value);
|
||||
Value appendObject();
|
||||
|
||||
std::string asString() const;
|
||||
std::int64_t asInt64() const;
|
||||
int asInt() const;
|
||||
double asDouble() const;
|
||||
bool asBool() const;
|
||||
|
||||
void write(const std::string& path, Version version = Version::v2) const;
|
||||
void writeBinary(const std::string& path, Version version = Version::v2) const;
|
||||
std::vector<std::uint8_t> toBinary(Version version = Version::v2) const;
|
||||
void refresh(const std::string& path);
|
||||
|
||||
private:
|
||||
struct Deleter { void operator()(ikv_node_t* node) const noexcept; };
|
||||
using Owner = std::shared_ptr<ikv_node_t>;
|
||||
|
||||
Value(Owner owner, ikv_node_t* node, ikv_node_t* parent, std::string key, bool read_only = false);
|
||||
static Value adopt(ikv_node_t* node);
|
||||
void require(ValueType expected, const char* operation) const;
|
||||
void requireMutable(const char* operation) const;
|
||||
void requireObjectParent(const char* operation) const;
|
||||
Value appended(std::size_t old_size);
|
||||
|
||||
Owner owner_;
|
||||
ikv_node_t* node_ = nullptr;
|
||||
ikv_node_t* parent_ = nullptr;
|
||||
std::string key_;
|
||||
bool read_only_ = false;
|
||||
};
|
||||
|
||||
} // namespace ikv
|
||||
254
src/value.cpp
Normal file
254
src/value.cpp
Normal file
@@ -0,0 +1,254 @@
|
||||
#include "ikvxx/ikvxx.hpp"
|
||||
|
||||
extern "C" {
|
||||
#include "ikv.h"
|
||||
}
|
||||
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
namespace ikv {
|
||||
namespace {
|
||||
|
||||
ikv_version_t nativeVersion(Version version) {
|
||||
return version == Version::v1 ? IKV_VERSION_1 : IKV_VERSION_2;
|
||||
}
|
||||
|
||||
ikv_type_t nativeType(ValueType type) {
|
||||
switch (type) {
|
||||
case stringValue: return IKV_STRING;
|
||||
case intValue: return IKV_INT;
|
||||
case realValue: return IKV_FLOAT;
|
||||
case booleanValue: return IKV_BOOL;
|
||||
case objectValue: return IKV_OBJECT;
|
||||
case arrayValue: return IKV_ARRAY;
|
||||
default: return IKV_NULL;
|
||||
}
|
||||
}
|
||||
|
||||
ValueType publicType(ikv_type_t type) noexcept {
|
||||
switch (type) {
|
||||
case IKV_STRING: return stringValue;
|
||||
case IKV_INT: return intValue;
|
||||
case IKV_FLOAT: return realValue;
|
||||
case IKV_BOOL: return booleanValue;
|
||||
case IKV_OBJECT: return objectValue;
|
||||
case IKV_ARRAY: return arrayValue;
|
||||
default: return nullValue;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Value::Deleter::operator()(ikv_node_t* node) const noexcept { ikv_free(node); }
|
||||
|
||||
Value::Value(ValueType type, std::string root_name) {
|
||||
ikv_node_t* node = nullptr;
|
||||
if (type == objectValue) node = ikv_create_object(root_name.c_str());
|
||||
else if (type == arrayValue) node = ikv_create_array(root_name.c_str(), IKV_NULL);
|
||||
else throw Error("iKv roots must be objects or arrays");
|
||||
if (!node) throw Error("failed to allocate iKv root");
|
||||
owner_.reset(node, Deleter{});
|
||||
node_ = node;
|
||||
}
|
||||
|
||||
Value::Value(Owner owner, ikv_node_t* node, ikv_node_t* parent, std::string key, bool read_only)
|
||||
: owner_(std::move(owner)), node_(node), parent_(parent), key_(std::move(key)), read_only_(read_only) {}
|
||||
|
||||
Value Value::adopt(ikv_node_t* node) {
|
||||
if (!node) throw Error("failed to parse iKv data");
|
||||
Owner owner(node, Deleter{});
|
||||
return Value(owner, node, nullptr, {});
|
||||
}
|
||||
|
||||
Value Value::parse(const std::string& text) { return adopt(ikv_parse_string(text.c_str())); }
|
||||
Value Value::load(const std::string& path) { return adopt(ikv_parse_file(path.c_str())); }
|
||||
Value Value::fromBinary(const void* data, std::size_t size) {
|
||||
if (!data && size != 0) throw Error("binary input is null");
|
||||
return adopt(ikvb_parse_memory(data, size));
|
||||
}
|
||||
Value Value::loadBinary(const std::string& path) { return adopt(ikvb_parse_file(path.c_str())); }
|
||||
|
||||
ValueType Value::type() const noexcept { return node_ ? publicType(ikv_node_type(node_)) : nullValue; }
|
||||
bool Value::isNull() const noexcept { return node_ == nullptr; }
|
||||
bool Value::isString() const noexcept { return type() == stringValue; }
|
||||
bool Value::isInt() const noexcept { return type() == intValue; }
|
||||
bool Value::isDouble() const noexcept { return type() == realValue; }
|
||||
bool Value::isBool() const noexcept { return type() == booleanValue; }
|
||||
bool Value::isObject() const noexcept { return type() == objectValue; }
|
||||
bool Value::isArray() const noexcept { return type() == arrayValue; }
|
||||
bool Value::empty() const { return size() == 0; }
|
||||
|
||||
std::size_t Value::size() const {
|
||||
if (isObject()) return ikv_object_size(node_);
|
||||
if (isArray()) return ikv_array_size(node_);
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string Value::name() const {
|
||||
const char* key = node_ ? ikv_node_key(node_) : nullptr;
|
||||
return key ? key : "";
|
||||
}
|
||||
|
||||
bool Value::isMember(const std::string& key) const {
|
||||
return isObject() && ikv_object_get(node_, key.c_str()) != nullptr;
|
||||
}
|
||||
|
||||
Value Value::operator[](const std::string& key) {
|
||||
require(objectValue, "object lookup");
|
||||
return Value(owner_, ikv_object_get(node_, key.c_str()), node_, key);
|
||||
}
|
||||
|
||||
Value Value::operator[](const char* key) {
|
||||
if (!key) throw Error("object key is null");
|
||||
return (*this)[std::string(key)];
|
||||
}
|
||||
|
||||
Value Value::operator[](const std::string& key) const {
|
||||
require(objectValue, "object lookup");
|
||||
return Value(owner_, ikv_object_get(node_, key.c_str()), nullptr, {}, true);
|
||||
}
|
||||
|
||||
Value Value::operator[](const char* key) const {
|
||||
if (!key) throw Error("object key is null");
|
||||
return (*this)[std::string(key)];
|
||||
}
|
||||
|
||||
Value Value::operator[](ArrayIndex index) {
|
||||
require(arrayValue, "array lookup");
|
||||
if (index >= ikv_array_size(node_)) throw std::out_of_range("iKv array index out of range");
|
||||
return Value(owner_, ikv_array_get(node_, index), node_, {});
|
||||
}
|
||||
|
||||
Value Value::operator[](int index) {
|
||||
if (index < 0) throw std::out_of_range("iKv array index out of range");
|
||||
return (*this)[static_cast<ArrayIndex>(index)];
|
||||
}
|
||||
|
||||
Value Value::operator[](ArrayIndex index) const {
|
||||
require(arrayValue, "array lookup");
|
||||
if (index >= ikv_array_size(node_)) throw std::out_of_range("iKv array index out of range");
|
||||
return Value(owner_, ikv_array_get(node_, index), node_, {}, true);
|
||||
}
|
||||
|
||||
Value Value::operator[](int index) const {
|
||||
if (index < 0) throw std::out_of_range("iKv array index out of range");
|
||||
return (*this)[static_cast<ArrayIndex>(index)];
|
||||
}
|
||||
|
||||
Value Value::get(const std::string& key, const Value& fallback) const {
|
||||
require(objectValue, "object lookup");
|
||||
ikv_node_t* child = ikv_object_get(node_, key.c_str());
|
||||
return child ? Value(owner_, child, nullptr, {}, true) : fallback;
|
||||
}
|
||||
|
||||
void Value::require(ValueType expected, const char* operation) const {
|
||||
if (!node_ || type() != expected) throw Error(std::string(operation) + ": incompatible value type");
|
||||
}
|
||||
|
||||
void Value::requireMutable(const char* operation) const {
|
||||
if (read_only_) throw Error(std::string(operation) + ": value is read-only");
|
||||
}
|
||||
|
||||
void Value::requireObjectParent(const char* operation) const {
|
||||
requireMutable(operation);
|
||||
if (!parent_ || ikv_node_type(parent_) != IKV_OBJECT || key_.empty())
|
||||
throw Error(std::string(operation) + ": assignment requires an object member");
|
||||
}
|
||||
|
||||
Value& Value::operator=(const std::string& value) {
|
||||
requireObjectParent("string assignment");
|
||||
ikv_object_set_string(parent_, key_.c_str(), value.c_str());
|
||||
node_ = ikv_object_get(parent_, key_.c_str());
|
||||
if (!node_) throw Error("string assignment failed");
|
||||
return *this;
|
||||
}
|
||||
Value& Value::operator=(const char* value) {
|
||||
if (!value) throw Error("string value is null");
|
||||
return *this = std::string(value);
|
||||
}
|
||||
Value& Value::operator=(std::int64_t value) {
|
||||
requireObjectParent("integer assignment");
|
||||
ikv_object_set_int(parent_, key_.c_str(), value);
|
||||
node_ = ikv_object_get(parent_, key_.c_str());
|
||||
if (!node_) throw Error("integer assignment failed");
|
||||
return *this;
|
||||
}
|
||||
Value& Value::operator=(int value) { return *this = static_cast<std::int64_t>(value); }
|
||||
Value& Value::operator=(double value) {
|
||||
requireObjectParent("real assignment");
|
||||
ikv_object_set_float(parent_, key_.c_str(), value);
|
||||
node_ = ikv_object_get(parent_, key_.c_str());
|
||||
if (!node_) throw Error("real assignment failed");
|
||||
return *this;
|
||||
}
|
||||
Value& Value::operator=(bool value) {
|
||||
requireObjectParent("boolean assignment");
|
||||
ikv_object_set_bool(parent_, key_.c_str(), value);
|
||||
node_ = ikv_object_get(parent_, key_.c_str());
|
||||
if (!node_) throw Error("boolean assignment failed");
|
||||
return *this;
|
||||
}
|
||||
|
||||
Value Value::makeObject(const std::string& key) {
|
||||
requireMutable("object creation");
|
||||
require(objectValue, "object creation");
|
||||
ikv_node_t* child = ikv_object_add_object(node_, key.c_str());
|
||||
if (!child) throw Error("failed to create object member");
|
||||
return Value(owner_, child, node_, key);
|
||||
}
|
||||
|
||||
Value Value::makeArray(const std::string& key, ValueType element_type) {
|
||||
requireMutable("array creation");
|
||||
require(objectValue, "array creation");
|
||||
if (element_type == arrayValue) throw Error("iKv does not support arrays of arrays");
|
||||
ikv_node_t* child = ikv_object_add_array(node_, key.c_str(), nativeType(element_type));
|
||||
if (!child) throw Error("failed to create array member");
|
||||
return Value(owner_, child, node_, key);
|
||||
}
|
||||
|
||||
Value Value::appended(std::size_t old_size) {
|
||||
if (ikv_array_size(node_) != old_size + 1) throw Error("array append rejected by its element type");
|
||||
return Value(owner_, ikv_array_get(node_, static_cast<std::uint32_t>(old_size)), node_, {});
|
||||
}
|
||||
Value Value::append(const std::string& value) { requireMutable("append"); require(arrayValue, "append"); auto n=size(); ikv_array_add_string(node_,value.c_str()); return appended(n); }
|
||||
Value Value::append(const char* value) { if(!value) throw Error("string value is null"); return append(std::string(value)); }
|
||||
Value Value::append(std::int64_t value) { requireMutable("append"); require(arrayValue, "append"); auto n=size(); ikv_array_add_int(node_,value); return appended(n); }
|
||||
Value Value::append(int value) { return append(static_cast<std::int64_t>(value)); }
|
||||
Value Value::append(double value) { requireMutable("append"); require(arrayValue, "append"); auto n=size(); ikv_array_add_float(node_,value); return appended(n); }
|
||||
Value Value::append(bool value) { requireMutable("append"); require(arrayValue, "append"); auto n=size(); ikv_array_add_bool(node_,value); return appended(n); }
|
||||
Value Value::appendObject() { requireMutable("append object"); require(arrayValue, "append object"); auto n=size(); if(!ikv_array_add_object(node_)) throw Error("object append rejected by array type"); return appended(n); }
|
||||
|
||||
std::string Value::asString() const { require(stringValue, "asString"); const char* s=ikv_as_string(node_); return s?s:""; }
|
||||
std::int64_t Value::asInt64() const { require(intValue, "asInt64"); return ikv_as_int(node_); }
|
||||
int Value::asInt() const {
|
||||
auto value = asInt64();
|
||||
if (value < std::numeric_limits<int>::min() || value > std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("iKv integer does not fit in int");
|
||||
return static_cast<int>(value);
|
||||
}
|
||||
double Value::asDouble() const { require(realValue, "asDouble"); return ikv_as_float(node_); }
|
||||
bool Value::asBool() const { require(booleanValue, "asBool"); return ikv_as_bool(node_); }
|
||||
|
||||
void Value::write(const std::string& path, Version version) const {
|
||||
if (!node_ || !ikv_write_file_version(path.c_str(), node_, nativeVersion(version))) throw Error("failed to write iKv file: " + path);
|
||||
}
|
||||
void Value::writeBinary(const std::string& path, Version version) const {
|
||||
if (!node_ || !ikvb_write_file_version(path.c_str(), node_, nativeVersion(version))) throw Error("failed to write binary iKv file: " + path);
|
||||
}
|
||||
std::vector<std::uint8_t> Value::toBinary(Version version) const {
|
||||
std::uint8_t* data = nullptr; std::uint32_t size = 0;
|
||||
if (!node_ || !ikvb_write_memory_version(node_, &data, &size, nativeVersion(version))) throw Error("failed to encode binary iKv data");
|
||||
std::unique_ptr<std::uint8_t, decltype(&std::free)> memory(data, &std::free);
|
||||
std::vector<std::uint8_t> result;
|
||||
if (size != 0) result.assign(data, data + size);
|
||||
return result;
|
||||
}
|
||||
void Value::refresh(const std::string& path) {
|
||||
requireMutable("refresh");
|
||||
if (!owner_ || node_ != owner_.get()) throw Error("refresh is only valid on a root value");
|
||||
if (!ikv_refresh_from_path(node_, path.c_str())) throw Error("failed to refresh iKv root from: " + path);
|
||||
}
|
||||
|
||||
} // namespace ikv
|
||||
48
tests/run.bat
Normal file
48
tests/run.bat
Normal file
@@ -0,0 +1,48 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
for %%I in ("%~dp0..") do set "ROOT_DIR=%%~fI"
|
||||
set "BUILD_DIR=%ROOT_DIR%\build-tests"
|
||||
set "CONFIG=%~1"
|
||||
|
||||
if not defined CONFIG set "CONFIG=Release"
|
||||
|
||||
where cmake >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo Error: CMake was not found in PATH.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
where ctest >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo Error: CTest was not found in PATH.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Configuring iKvxx tests in "%BUILD_DIR%"...
|
||||
cmake -S "%ROOT_DIR%" -B "%BUILD_DIR%" ^
|
||||
-DIKVXX_BUILD_TESTS=ON ^
|
||||
-DIKVXX_INSTALL=OFF
|
||||
if errorlevel 1 (
|
||||
echo Error: CMake configuration failed.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Building iKvxx tests using the %CONFIG% configuration...
|
||||
cmake --build "%BUILD_DIR%" --config "%CONFIG%" --parallel
|
||||
if errorlevel 1 (
|
||||
echo Error: Test build failed.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Running iKvxx tests...
|
||||
ctest --test-dir "%BUILD_DIR%" ^
|
||||
--build-config "%CONFIG%" ^
|
||||
--output-on-failure
|
||||
if errorlevel 1 (
|
||||
echo Error: One or more tests failed.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo All iKvxx tests passed.
|
||||
exit /b 0
|
||||
381
tests/value_tests.cpp
Normal file
381
tests/value_tests.cpp
Normal file
@@ -0,0 +1,381 @@
|
||||
#include "ikvxx/ikvxx.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
int exception_expectations = 0;
|
||||
|
||||
void check(bool condition, const char* expression, int line) {
|
||||
if (!condition) {
|
||||
std::cerr << "line " << line << ": CHECK(" << expression << ") failed\n";
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
#define CHECK(expression) check(static_cast<bool>(expression), #expression, __LINE__)
|
||||
|
||||
template <typename Function>
|
||||
void expectThrow(Function&& function) {
|
||||
const int current = ++exception_expectations;
|
||||
try {
|
||||
function();
|
||||
std::cerr << "expected exception case " << current << " did not throw\n";
|
||||
CHECK(false);
|
||||
} catch (const std::exception&) {
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path testPath(const std::string& name) {
|
||||
return std::filesystem::current_path() / ("ikvxx-test-" + name);
|
||||
}
|
||||
|
||||
struct FileCleanup {
|
||||
explicit FileCleanup(std::filesystem::path value) : path(std::move(value)) {}
|
||||
~FileCleanup() { std::error_code ignored; std::filesystem::remove(path, ignored); }
|
||||
std::filesystem::path path;
|
||||
};
|
||||
|
||||
ikv::Value makeScalarTree() {
|
||||
ikv::Value root(ikv::objectValue, "types");
|
||||
root["string"] = std::string("value");
|
||||
root["int"] = std::int64_t{-9};
|
||||
root["real"] = 2.5;
|
||||
root["bool"] = true;
|
||||
root.makeObject("object");
|
||||
root.makeArray("array");
|
||||
return root;
|
||||
}
|
||||
|
||||
void testConstructionAndTypeInspection() {
|
||||
for (const std::string& name : {"root", "", "save data"}) {
|
||||
ikv::Value object(ikv::objectValue, name);
|
||||
CHECK(object.type() == ikv::objectValue);
|
||||
CHECK(object.isObject());
|
||||
CHECK(!object.isArray());
|
||||
CHECK(object.name() == name);
|
||||
CHECK(object.empty());
|
||||
CHECK(object.size() == 0);
|
||||
}
|
||||
|
||||
for (const std::string& name : {"items", "", "typed values"}) {
|
||||
ikv::Value array(ikv::arrayValue, name);
|
||||
CHECK(array.type() == ikv::arrayValue);
|
||||
CHECK(array.isArray());
|
||||
CHECK(!array.isObject());
|
||||
CHECK(array.name() == name);
|
||||
CHECK(array.empty());
|
||||
CHECK(array.size() == 0);
|
||||
}
|
||||
|
||||
expectThrow([] { ikv::Value value(ikv::stringValue); });
|
||||
expectThrow([] { ikv::Value value(ikv::intValue); });
|
||||
expectThrow([] { ikv::Value value(ikv::nullValue); });
|
||||
|
||||
auto root = makeScalarTree();
|
||||
auto missing = root["missing"];
|
||||
CHECK(missing.type() == ikv::nullValue);
|
||||
CHECK(missing.isNull());
|
||||
CHECK(!root.isNull());
|
||||
CHECK(!root["array"].isNull());
|
||||
CHECK(missing.empty());
|
||||
CHECK(missing.size() == 0);
|
||||
|
||||
struct ExpectedType { const char* key; ikv::ValueType type; };
|
||||
const ExpectedType expected[] = {
|
||||
{"string", ikv::stringValue}, {"int", ikv::intValue},
|
||||
{"real", ikv::realValue}, {"bool", ikv::booleanValue},
|
||||
{"object", ikv::objectValue}, {"array", ikv::arrayValue}
|
||||
};
|
||||
for (const auto& entry : expected) CHECK(root[entry.key].type() == entry.type);
|
||||
|
||||
CHECK(root["string"].isString());
|
||||
CHECK(!root["int"].isString());
|
||||
CHECK(!root["missing"].isString());
|
||||
CHECK(root["int"].isInt());
|
||||
CHECK(!root["real"].isInt());
|
||||
CHECK(!root["missing"].isInt());
|
||||
CHECK(root["real"].isDouble());
|
||||
CHECK(!root["int"].isDouble());
|
||||
CHECK(!root["missing"].isDouble());
|
||||
CHECK(root["bool"].isBool());
|
||||
CHECK(!root["string"].isBool());
|
||||
CHECK(!root["missing"].isBool());
|
||||
CHECK(root["object"].isObject());
|
||||
CHECK(!root["array"].isObject());
|
||||
CHECK(!root["missing"].isObject());
|
||||
CHECK(root["array"].isArray());
|
||||
CHECK(!root["object"].isArray());
|
||||
CHECK(!root["missing"].isArray());
|
||||
}
|
||||
|
||||
void testAssignmentsAndConversions() {
|
||||
ikv::Value root;
|
||||
|
||||
const std::vector<std::string> strings = {"", "hello", "line\nquote\""};
|
||||
for (std::size_t i = 0; i < strings.size(); ++i) {
|
||||
root["std-string-" + std::to_string(i)] = strings[i];
|
||||
CHECK(root["std-string-" + std::to_string(i)].asString() == strings[i]);
|
||||
}
|
||||
for (const char* value : {"alpha", "", "unicode-ish"}) {
|
||||
root[std::string("c-string-") + std::to_string(root.size())] = value;
|
||||
}
|
||||
CHECK(root["c-string-3"].asString() == "alpha");
|
||||
CHECK(root["c-string-4"].asString().empty());
|
||||
CHECK(root["c-string-5"].asString() == "unicode-ish");
|
||||
expectThrow([&] { root["bad"] = static_cast<const char*>(nullptr); });
|
||||
|
||||
const std::int64_t wide_values[] = {
|
||||
std::numeric_limits<std::int64_t>::min(), 0, std::numeric_limits<std::int64_t>::max()
|
||||
};
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
root["wide-" + std::to_string(i)] = wide_values[i];
|
||||
CHECK(root["wide-" + std::to_string(i)].asInt64() == wide_values[i]);
|
||||
}
|
||||
for (int value : {-17, 0, 42}) {
|
||||
const auto key = "int-" + std::to_string(value);
|
||||
root[key] = value;
|
||||
CHECK(root[key].asInt() == value);
|
||||
}
|
||||
|
||||
for (double value : {-3.5, 0.0, 1.0 / 3.0}) {
|
||||
const auto key = "real-" + std::to_string(root.size());
|
||||
root[key] = value;
|
||||
CHECK(std::abs(root[key].asDouble() - value) < 1e-12);
|
||||
}
|
||||
root["bool-false"] = false;
|
||||
root["bool-true"] = true;
|
||||
root["bool-replaced"] = false;
|
||||
root["bool-replaced"] = true;
|
||||
CHECK(!root["bool-false"].asBool());
|
||||
CHECK(root["bool-true"].asBool());
|
||||
CHECK(root["bool-replaced"].asBool());
|
||||
|
||||
root["replace"] = "first";
|
||||
root["replace"] = 2;
|
||||
root["replace"] = 4.5;
|
||||
CHECK(root["replace"].isDouble());
|
||||
CHECK(std::abs(root["replace"].asDouble() - 4.5) < 1e-12);
|
||||
|
||||
root["too-large"] = std::numeric_limits<std::int64_t>::max();
|
||||
root["too-small"] = std::numeric_limits<std::int64_t>::min();
|
||||
expectThrow([&] { (void)root["too-large"].asInt(); });
|
||||
expectThrow([&] { (void)root["too-small"].asInt(); });
|
||||
expectThrow([&] { (void)root["bool-true"].asString(); });
|
||||
expectThrow([&] { (void)root["bool-true"].asInt64(); });
|
||||
expectThrow([&] { (void)root["bool-true"].asDouble(); });
|
||||
expectThrow([&] { (void)root["replace"].asBool(); });
|
||||
}
|
||||
|
||||
void testObjectsMembersAndLookupOverloads() {
|
||||
ikv::Value root;
|
||||
for (const std::string& key : {"first", "", "with spaces"}) {
|
||||
auto object = root.makeObject(key);
|
||||
object["value"] = key;
|
||||
CHECK(object.isObject());
|
||||
CHECK(object.name() == key);
|
||||
}
|
||||
CHECK(root.isMember("first"));
|
||||
CHECK(root.isMember(""));
|
||||
CHECK(root.isMember("with spaces"));
|
||||
CHECK(!root.isMember("missing"));
|
||||
CHECK(!root["first"].isMember("missing"));
|
||||
|
||||
std::string first = "first";
|
||||
CHECK(root[first][std::string("value")].asString() == "first");
|
||||
CHECK(root["with spaces"]["value"].asString() == "with spaces");
|
||||
expectThrow([&] { (void)root[static_cast<const char*>(nullptr)]; });
|
||||
|
||||
const ikv::Value& const_root = root;
|
||||
CHECK(const_root[first][std::string("value")].asString() == "first");
|
||||
const std::string spaced = "with spaces";
|
||||
const std::string absent = "missing";
|
||||
CHECK(const_root[spaced].isObject());
|
||||
CHECK(const_root[absent].isNull());
|
||||
CHECK(const_root["with spaces"]["value"].asString() == "with spaces");
|
||||
CHECK(const_root["missing"].isNull());
|
||||
expectThrow([&] { (void)const_root[static_cast<const char*>(nullptr)]; });
|
||||
|
||||
ikv::Value fallback(ikv::objectValue, "fallback");
|
||||
CHECK(const_root.get("first", fallback).name() == "first");
|
||||
CHECK(const_root.get("missing", fallback).name() == "fallback");
|
||||
CHECK(const_root.get("", fallback).name().empty());
|
||||
|
||||
expectThrow([&] { root["scalar"] = 1; root["scalar"].makeObject("bad"); });
|
||||
expectThrow([&] { root["scalar"].makeArray("bad"); });
|
||||
const ikv::Value& read_only = root;
|
||||
expectThrow([&] { read_only["first"].makeObject("bad"); });
|
||||
}
|
||||
|
||||
void testArraysAndIndexOverloads() {
|
||||
ikv::Value root;
|
||||
auto mixed = root.makeArray("mixed");
|
||||
auto strings = root.makeArray("strings", ikv::stringValue);
|
||||
auto integers = root.makeArray("integers", ikv::intValue);
|
||||
auto reals = root.makeArray("reals", ikv::realValue);
|
||||
auto booleans = root.makeArray("booleans", ikv::booleanValue);
|
||||
auto objects = root.makeArray("objects", ikv::objectValue);
|
||||
|
||||
for (const std::string& value : {"", "two", "three"}) strings.append(value);
|
||||
for (const char* value : {"four", "five", "six"}) mixed.append(value);
|
||||
for (std::int64_t value : {-9000000000LL, 0LL, 9000000000LL}) integers.append(value);
|
||||
for (int value : {-2, 0, 7}) mixed.append(value);
|
||||
for (double value : {-1.5, 0.0, 9.25}) reals.append(value);
|
||||
for (bool value : {false, true, false}) booleans.append(value);
|
||||
for (int i = 0; i < 3; ++i) objects.appendObject()["index"] = i;
|
||||
|
||||
CHECK(strings.size() == 3);
|
||||
CHECK(strings[0].asString().empty());
|
||||
CHECK(strings[1].asString() == "two");
|
||||
CHECK(strings[static_cast<ikv::Value::ArrayIndex>(2)].asString() == "three");
|
||||
CHECK(strings[static_cast<ikv::Value::ArrayIndex>(0)].asString().empty());
|
||||
CHECK(strings[static_cast<ikv::Value::ArrayIndex>(1)].asString() == "two");
|
||||
CHECK(integers[0].asInt64() == -9000000000LL);
|
||||
CHECK(integers[1].asInt64() == 0);
|
||||
CHECK(integers[2].asInt64() == 9000000000LL);
|
||||
CHECK(std::abs(reals[0].asDouble() + 1.5) < 1e-12);
|
||||
CHECK(std::abs(reals[1].asDouble()) < 1e-12);
|
||||
CHECK(std::abs(reals[2].asDouble() - 9.25) < 1e-12);
|
||||
CHECK(!booleans[0].asBool());
|
||||
CHECK(booleans[1].asBool());
|
||||
CHECK(!booleans[2].asBool());
|
||||
CHECK(objects[0]["index"].asInt() == 0);
|
||||
CHECK(objects[1]["index"].asInt() == 1);
|
||||
CHECK(objects[2]["index"].asInt() == 2);
|
||||
|
||||
const ikv::Value& const_strings = strings;
|
||||
CHECK(const_strings[0].asString().empty());
|
||||
CHECK(const_strings[1].asString() == "two");
|
||||
CHECK(const_strings[2].asString() == "three");
|
||||
CHECK(const_strings[static_cast<ikv::Value::ArrayIndex>(0)].asString().empty());
|
||||
CHECK(const_strings[static_cast<ikv::Value::ArrayIndex>(1)].asString() == "two");
|
||||
CHECK(const_strings[static_cast<ikv::Value::ArrayIndex>(2)].asString() == "three");
|
||||
expectThrow([&] { (void)strings[-1]; });
|
||||
expectThrow([&] { (void)strings[3]; });
|
||||
expectThrow([&] { (void)const_strings[-1]; });
|
||||
expectThrow([&] { (void)const_strings[3]; });
|
||||
|
||||
expectThrow([&] { strings.append(7); });
|
||||
expectThrow([&] { integers.append("wrong"); });
|
||||
expectThrow([&] { objects.append(false); });
|
||||
expectThrow([&] { mixed.append(static_cast<const char*>(nullptr)); });
|
||||
expectThrow([&] { root.makeArray("nested", ikv::arrayValue); });
|
||||
expectThrow([&] { strings.appendObject(); });
|
||||
|
||||
const ikv::Value& read_only = root;
|
||||
expectThrow([&] { read_only["mixed"].append(1); });
|
||||
expectThrow([&] { read_only["mixed"].append("value"); });
|
||||
expectThrow([&] { read_only["mixed"].appendObject(); });
|
||||
}
|
||||
|
||||
void testTextParsingAndFiles() {
|
||||
const std::string documents[] = {
|
||||
"ikv2 \"root\"\n{\n \"name\" \"test\"\n}\n",
|
||||
"ikv1 \"legacy\"\n{\n \"n\" -7\n}\n",
|
||||
"ikv2 \"nested\"\n{\n \"child\" { \"ok\" true }\n}\n"
|
||||
};
|
||||
CHECK(ikv::Value::parse(documents[0])["name"].asString() == "test");
|
||||
CHECK(ikv::Value::parse(documents[1])["n"].asInt() == -7);
|
||||
CHECK(ikv::Value::parse(documents[2])["child"]["ok"].asBool());
|
||||
expectThrow([] { (void)ikv::Value::parse("ikv2 \"root\"\n"); });
|
||||
expectThrow([] { (void)ikv::Value::parse("not ikv"); });
|
||||
expectThrow([] { (void)ikv::Value::parse("ikv2 \"root\"\n{\n \"value\" 1\n"); });
|
||||
|
||||
const FileCleanup v1(testPath("text-v1.ikv"));
|
||||
const FileCleanup v2(testPath("text-v2.ikv"));
|
||||
const FileCleanup bad(testPath("text-bad.ikv"));
|
||||
ikv::Value source;
|
||||
source["value"] = 11;
|
||||
source.write(v1.path.string(), ikv::Version::v1);
|
||||
source.write(v2.path.string());
|
||||
CHECK(ikv::Value::load(v1.path.string())["value"].asInt() == 11);
|
||||
CHECK(ikv::Value::load(v2.path.string())["value"].asInt() == 11);
|
||||
expectThrow([&] { (void)ikv::Value::load(testPath("missing.ikv").string()); });
|
||||
expectThrow([&] { source.write(testPath("missing-dir/file.ikv").string()); });
|
||||
|
||||
{ std::ofstream output(bad.path); output << "broken"; }
|
||||
expectThrow([&] { (void)ikv::Value::load(bad.path.string()); });
|
||||
}
|
||||
|
||||
void testBinaryMemoryAndFiles() {
|
||||
ikv::Value source(ikv::objectValue, "binary");
|
||||
source["number"] = 73;
|
||||
source["text"] = "payload";
|
||||
source.makeArray("flags", ikv::booleanValue).append(true);
|
||||
|
||||
const auto v1 = source.toBinary(ikv::Version::v1);
|
||||
const auto v2 = source.toBinary();
|
||||
const auto v2_explicit = source.toBinary(ikv::Version::v2);
|
||||
CHECK(!v1.empty());
|
||||
CHECK(!v2.empty());
|
||||
CHECK(v2_explicit == v2);
|
||||
CHECK(ikv::Value::fromBinary(v1.data(), v1.size())["number"].asInt() == 73);
|
||||
CHECK(ikv::Value::fromBinary(v2.data(), v2.size())["text"].asString() == "payload");
|
||||
CHECK(ikv::Value::fromBinary(v2.data(), v2.size())["flags"][0].asBool());
|
||||
const std::uint8_t invalid[] = {0, 1, 2, 3, 4};
|
||||
expectThrow([&] { (void)ikv::Value::fromBinary(invalid, sizeof(invalid)); });
|
||||
expectThrow([] { (void)ikv::Value::fromBinary(nullptr, 1); });
|
||||
expectThrow([] { (void)ikv::Value::fromBinary(nullptr, 0); });
|
||||
|
||||
const FileCleanup v1_file(testPath("binary-v1.ikvb"));
|
||||
const FileCleanup v2_file(testPath("binary-v2.ikvb"));
|
||||
const FileCleanup bad_file(testPath("binary-bad.ikvb"));
|
||||
source.writeBinary(v1_file.path.string(), ikv::Version::v1);
|
||||
source.writeBinary(v2_file.path.string());
|
||||
CHECK(ikv::Value::loadBinary(v1_file.path.string())["number"].asInt() == 73);
|
||||
CHECK(ikv::Value::loadBinary(v2_file.path.string())["text"].asString() == "payload");
|
||||
expectThrow([&] { source.writeBinary(testPath("missing-dir/file.ikvb").string()); });
|
||||
expectThrow([&] { (void)ikv::Value::loadBinary(testPath("missing.ikvb").string()); });
|
||||
{ std::ofstream output(bad_file.path, std::ios::binary); output << "broken"; }
|
||||
expectThrow([&] { (void)ikv::Value::loadBinary(bad_file.path.string()); });
|
||||
}
|
||||
|
||||
void testRefreshAndOwnership() {
|
||||
const FileCleanup first(testPath("refresh-first.ikv"));
|
||||
const FileCleanup second(testPath("refresh-second.ikvb"));
|
||||
ikv::Value text_source;
|
||||
text_source["value"] = 1;
|
||||
text_source.write(first.path.string());
|
||||
ikv::Value binary_source;
|
||||
binary_source["value"] = 2;
|
||||
binary_source.writeBinary(second.path.string());
|
||||
|
||||
ikv::Value root;
|
||||
root["value"] = 0;
|
||||
auto shared = root;
|
||||
root.refresh(first.path.string());
|
||||
CHECK(root["value"].asInt() == 1);
|
||||
CHECK(shared["value"].asInt() == 1);
|
||||
root.refresh(second.path.string());
|
||||
CHECK(root["value"].asInt() == 2);
|
||||
expectThrow([&] { root.refresh(testPath("missing-refresh.ikv").string()); });
|
||||
CHECK(root["value"].asInt() == 2);
|
||||
|
||||
auto child = root["value"];
|
||||
expectThrow([&] { child.refresh(first.path.string()); });
|
||||
const ikv::Value& read_only = root;
|
||||
expectThrow([&] { read_only["value"] = 3; });
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
testConstructionAndTypeInspection();
|
||||
testAssignmentsAndConversions();
|
||||
testObjectsMembersAndLookupOverloads();
|
||||
testArraysAndIndexOverloads();
|
||||
testTextParsingAndFiles();
|
||||
testBinaryMemoryAndFiles();
|
||||
testRefreshAndOwnership();
|
||||
if (failures != 0) std::cerr << failures << " test(s) failed\n";
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
1
third_party/iKv
vendored
Submodule
1
third_party/iKv
vendored
Submodule
Submodule third_party/iKv added at d0b02f4735
Reference in New Issue
Block a user