Skip to content

orefkov/simjson

Repository files navigation

simjson - the simplest library for working with JSON

CMake on multiple platforms

Designed to work with JSON when using the simstr library.

Version 1.2.3.

On Russian | По-русски

This library contains a simple implementation of a simple JsonValue object for working with JSON using string objects of the simstr library, since other libraries mainly work with std::string or raw const char*. The task was not to somehow compete in performance or optimality with other libraries, I mainly use it for working with small config files - read, modify, write. However, it also copes quite well with large files.

For json objects, std::unordered_map is used, in the form of hashStrMap<K, JsonValueTemp<K>>, for arrays - std::vector<JsonValueTemp<K>>, strings are stored in sstring<K>.

Key features of the library

  • Works with all simstr strings.
  • Supports working with strings char, char16_t, char32_t, wchar_t.
  • Convenient creation, reading and modification of json values.
  • Copying JSON values such as arrays and objects is done by reference (only shared_ptr is copied).
  • Possible "deep" copying aka cloning of JSON values, in this case a full copy is created for arrays and objects.
  • "Merging" one JSON object with another, with the ability to set priority.
  • Extended work with numbers - allows you to use int64_t and double.
  • Parsing a string into Json, with support for partial parsing.
  • Serializing json to a string, with options - sorting keys, "readable" output, number of indents and symbol indentation with "readable" output.

Main objects of the library

  • JsonValueTempl - Json value type, parameter K specifies the type of characters used in the string. Aliases:
    • JsonValue - for char strings
    • JsonValueU - for char16_t strings
    • JsonValueUU - for char32_t strings
    • JsonValueW - for wchar_t strings
  • StreamedJsonParser - parser of a string into JSON, supporting "partial" parsing. For example, data comes in portions from the network, you feed it to the parser as it arrives, until it either parses, or throws an error.

Usage

simjson consists of a header file and one source file. You can connect as a CMake project via add_subdirectory (the simjson library), you can simply include the files in your project. Building also requires simstr (when using CMake downloads automatically).

simjson requires a compiler with support for a standard no lower than C++20 (concepts are used).

Usage examples

Creating, reading

    // Simple json, equal to 1.
    JsonValue json = 1; // int64
    stringa text = json.store(); // "1"

    // Initialization of the object, ""_h - slightly optimizes the key, calculating the hash during compilation
    JsonValue obj = {
        {"Key1"_h, 1},
        {"Key2"_h, true},
        {"Key3", {1, 2, Json::null, "test", false}}, // here will be an array [1, 2, null, "test", false]
        {"Key4"_h, {
            {"subkey1"_h, true},
            {"subkey2", "subkey"},
        }},
    };

    // Empty object
    JsonValue test;
    // Saving keys, immediately several levels
    test["a"_h]["b"_h]["c"_h] = 10;
    text = json.store(); // {"a":{"b":{"c":10}}}

    // Converts a json object to an array
    test[0] = "value";
    test[-1] = true;  // When using -1 - the value is added to the end of the array
    test[10] = false; // Will increase the array size to 11

    // Read value
    JsonValue config;
    ....
    stringa log_path = config("instance"_h, "base"_h, "log"_h, "path"_h).text().value_or("./log.txt");
    stringa work_dir = config("instance"_h, "base"_h, "work_dir"_h).text_or_throw<std::runtime_error>(
      "not specified work dir");

Example: Setting default values, reading from a file and merging with default values.

JsonValue json_config = {
    {"section1"_h, {
        {"sub1"_h, {
            {"path"_h, "."},
            {"workers"_h, 10},
        }},
        {"sub2"_h, {
            {"check"_h, true},
        }},
        {"key1"_h, true},
        {"key2"_h, false},
    }},
    {"section2"_h, {
        {"key1"_h, false},
        {"key2"_h, true},
    }},
};

inline bool is_file_exist(stra path) {
    auto status = std::filesystem::status(path.to_sv());
    return std::filesystem::status_known(status) && std::filesystem::is_regular_file(status);
}

void read_config_from_file(ssa folder, ssa file_name) {
    stringa path_checked = folder + e_if(folder(-1) != PATH_SEPARATOR, PATH_SEPARATOR);
    if (!file_name.is_empty()) {
        lstringa<MAX_PATH> fullPath = path_checked + file_name;
        stringa config;
        if (is_file_exist(fullPath)) {
            config = get_file_content(fullPath);

            auto [readed, error, line, col] = JsonValue::parse(config);

            if (error != JsonParseResult::Success) {
                std::cerr << "Error in parse config file " << fullPath <<
                  " at line " << line << ", col " << col << std::endl;
                throw std::runtime_error{"Error parse config file"};
            }
            json_config.merge(readed);
        }
        // We may have new keys in the default config, and I would like their default values
        // also drop to the file, to make it easier to edit later.
        stringa new_config = json_config.store(true, true, ' ', 4);
        if (new_config != config) {
            // New keys have been added to the default config
            std::ofstream file(fullPath.c_str(), std::ios::binary | std::ios::trunc);
            if (!file.is_open()) {
                std::cerr << "Error in create config file " << fullPath << std::endl;
            } else {
                file.write(new_config.c_str(), new_config.length());
            }
        }
    }
    json_config["runtime"_h]["location"_h]["base_dir"_h] = std::move(path_checked);
}

Example - initialization from standard containers

    // Arrays
    std::vector<int> vals = {1, 2, 3, 4};
    JsonValue json = vals;
    EXPECT_EQ(json.type(), Json::Array);
    EXPECT_EQ(json.store(), "[1,2,3,4]");

    JsonValue json1 = std::array<int, 4>{4, 3, 2, 1};
    EXPECT_EQ(json1.type(), Json::Array);
    EXPECT_EQ(json1.store(), "[4,3,2,1]");

    std::list<stringa> texts = {"one", "two", "three"};
    JsonValue json2 = texts;
    EXPECT_EQ(json2.type(), Json::Array);
    EXPECT_EQ(json2.store(), "[\"one\",\"two\",\"three\"]");

    // Key-value
    hashStrMapA<int> vals = {
        {"one"_h, 1}, {"two"_h, 2}, {"three"_h, 3}, {"four"_h, 4}
    };
    JsonValue json = vals;
    EXPECT_EQ(json.type(), Json::Object);
    EXPECT_EQ(json.store(false, true), "{\"four\":4,\"one\":1,\"three\":3,\"two\":2}");

    std::map<stringa, int> vals1 = {
        {"one", 1}, {"two", 2}, {"three", 3}, {"four", 4}
    };
    JsonValue json1 = vals1;
    EXPECT_EQ(json1.type(), Json::Object);
    EXPECT_EQ(json1.store(false, true), "{\"four\":4,\"one\":1,\"three\":3,\"two\":2}");

    std::vector<std::pair<lstringa<20>, int>> vals2 = {
        {"one", 1}, {"two", 2}, {"three", 3}, {"four", 4}
    };
    JsonValue json2 = vals2;
    EXPECT_EQ(json2.type(), Json::Object);
    EXPECT_EQ(json2.store(false, true), "{\"four\":4,\"one\":1,\"three\":3,\"two\":2}");

Generated documentation

Located here