feat(bindings): add modern C++ interface for iKv
Some checks failed
C++ bindings / gcc / Debug (push) Failing after 10s
C++ bindings / clang / Debug (push) Failing after 17s
C++ bindings / clang / Release (push) Failing after 10s
C++ bindings / gcc / Release (push) Failing after 16s

This commit is contained in:
2026-06-17 18:57:54 -05:00
parent a56451a01f
commit e651ae1575
11 changed files with 1325 additions and 29 deletions

View 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
View File

@@ -1,34 +1,110 @@
# ---> C++ # Build and installation trees
# Prerequisites /build/
*.d /build-*/
/cmake-build-*/
/out/
/dist/
/install/
# Compiled Object files # CMake-generated files from accidental in-source builds
*.slo /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 *.lo
*.mod
*.o *.o
*.obj *.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 *.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
View 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
View 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
View File

@@ -1,3 +1,310 @@
# iKvxx # 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).

View 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
View 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
View 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
View 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
View 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

Submodule third_party/iKv added at d0b02f4735