1
\$\begingroup\$

I want to make an attempt to write my own simple RPG. As a preparation for this task I created a class which I could use to store/retrieve my in-game objects data to/from disc. I chose JSON format as it is very simple and has good library support in C++.

While I was working on it, I thought that it could be useful to have other data formats (YAML? XML? INI?), but I decided to leave it for the future. I want to polish what I have now, before I move on to extending DataManager possibilities.

Class definition:

#ifndef DATAMANAGER_HPP
#define DATAMANAGER_HPP

#include <filesystem>
#include <iosfwd>
#include <map>

#include <json.hpp>

//Two user defined literals which might be useful for defining
//objects of class path and directory_entry from <filesystem>
std::experimental::filesystem::path operator""_p(const char* p, std::size_t len);
std::experimental::filesystem::directory_entry operator""_d(const char* d, std::size_t len);

namespace utils
{

    //both added for convenience
    namespace fs = std::experimental::filesystem;
    using json = nlohmann::json;


    //Class handling data description file I/O
    class DataManager
    {
        //as operator<< requires access to private data, it's declared friend
        friend std::ostream& operator<<(std::ostream&, DataManager const&);

    public:
        //No Parameter Constructor.
        //Sets default directory where data will be stored
        //to "working dir/data".
        DataManager();

        //Constructor with filesystem::path parameter.
        //Sets default directory where data will be stored
        //to data_dir.
        DataManager(fs::path data_dir);

        //Trivial destructor.
        ~DataManager();

        //This function clears DataManager object.
        //Only data held by it is cleared.
        void clear() { m_data.clear(); m_is_empty = true; }


        //Retrieves data by obj_id.
        //Data is returned in json format.
        json get(std::string obj_id, std::string file = "settings") const;

        //Retrieves data by obj_id.
        //Data is returned in std::string which contains json data destription.
        std::string get_str(std::string obj_id, std::string file = "settings") const;


        //Adds data for object with obj_id to file.
        void add(std::string obj_id, json data, std::string file = "settings");
        void add(std::string obj_id, std::string json_str, std::string file = "settings");

        //Three functions which read data from files (either single,
        //or a several from one directory) and stores them in internal map.
        //If no file_location is supplied, default data dir is used.
        //If file_location does not exist, no action is taken.
        //file_loaction can be specified either as absolute path,
        //of relatively to default data directory.
        //If read will be called twice, for two files with the same name,
        //but in two different directories, the files will be effectively MERGED as far as
        //internal data storage is concerned (which means that write function will
        //store them in the same file!).
        void read(std::vector<std::string> files, fs::path file_location = fs::path{});
        void read(std::vector<fs::path> files, fs::path file_location = fs::path{});
        void read(fs::path file, fs::path file_location = fs::path{});

        //Reads all files (with proper format) from selected directory.
        //Directory can be specified either as absolute path,
        //of relatively to default data directory. 
        //Searching for files can be recursively or not.
        //Files relative loaction to data directory is kept.
        //This means that function write will recreate directory
        //structure relative to output directory.
        void read_dir(std::string directory, bool recursive = true);
        void read_dir(fs::directory_entry directory, bool recursive = true);

        //Stores all data from map on HDD.
        //If data_dir is specified it is used as root
        //for all files and subdirectories.
        //If no data_dir is given, function uses
        //data directory set by constructor.
        //Directory can be specified either as absolute path,
        //of relatively to default data directory. 
        void write(fs::path data_dir = fs::path{}) const;

        //Returns parent path (absolute) for data directory.
        std::string root_path() const { return m_data_dir.path().parent_path().generic_string(); }

        //Returns absolute path to directory where data will be stored to/read from
        //unless specified otherwise.
        std::string data_dir() const { return m_data_dir.path().filename().generic_string(); }

        //Checks if there is any data in internal storage.
        bool is_empty() const noexcept { return m_is_empty; }

    private:

        //internal functions - code reuse
        //or shortening other functions
        void read_impl(fs::path&& file_name);
        void read_recursive(fs::path dir);
        void read_non_recursive(fs::path dir);

        void write_impl(fs::path file, std::ofstream& out_file) const;

        bool m_is_empty = true;

        //Multimap which stores all data passed to DataManager
        std::map <std::string, json> m_data;

        //Default data directory. Set during object construction.
        fs::directory_entry m_data_dir;

        //This member is used in write function
        //so it has to be mutable
        //It's only internal helper variable.
        //Its mutability is not relevant for users.
        //It's goal is to keep track of current I/O directory,
        //so that we could store only relative paths in map.
        mutable fs::path m_tmp_data_dir = fs::path{};

        //extension of the files used for storage
        std::string m_file_ext = ".json";
    };
}

#endif //DATAMANAGER_HPP

Class implementation:

#include "DataManager.hpp"

#include <fstream>

std::experimental::filesystem::path operator""_p(const char* p, std::size_t len)
{
    return std::experimental::filesystem::path{ p };
}

std::experimental::filesystem::directory_entry operator""_d(const char* d, std::size_t len)
{
    return std::experimental::filesystem::directory_entry{ d };
}

namespace utils
{
    namespace
    {
        //MSVC 2015 lacks some functions in <filesystem> so I implemented
        //very simple substitute that just gets job done
        fs::path lexically_relative(fs::path data_dir, fs::path root_dir)
        {
            std::string result = data_dir.generic_string().substr(root_dir.generic_string().length());
            if (result.front() == '\\' || result.front() == '/')
                result = result.substr(1);
            return fs::path{ result };
        }
    }

    DataManager::DataManager() :DataManager(fs::current_path() / fs::path{ "data" })
    { }

    DataManager::DataManager(fs::path data_dir)
    {
        data_dir = data_dir.is_relative() ? fs::canonical(data_dir, fs::current_path()) : data_dir;
        m_data_dir = fs::directory_entry{ data_dir };
    }

    DataManager::~DataManager()
    {
    }

    json DataManager::get(std::string obj_id, std::string file) const
    {
        if (m_is_empty || (m_data.count(file) == 0))
            return json{};

        return m_data.at(file)[obj_id];
    }

    std::string DataManager::get_str(std::string obj_id, std::string file) const
    {
        return get(obj_id, file).dump();
    }

    void DataManager::add(std::string obj_id, json data, std::string file)
    {
        m_data[file] += json::object_t::value_type(obj_id, data);
        m_is_empty = false;
    }

    void DataManager::add(std::string obj_id, std::string json_str, std::string file)
    {
        add(obj_id, json::parse(json_str.c_str()), file);
    }

    void DataManager::read(fs::path file, fs::path file_location)
    {
        file_location = file_location.empty() ? m_data_dir.path() : file_location;
        file_location = file_location.is_relative() ? fs::canonical(file_location, m_data_dir.path()) : file_location;

        if (!fs::exists(file_location))
            return;

        read_impl(file_location / file.replace_extension(m_file_ext));
    }

    void DataManager::read(std::vector<std::string> files, fs::path files_location)
    {
        files_location = files_location.empty() ? m_data_dir.path() : files_location;
        files_location = files_location.is_relative() ? fs::canonical(files_location, m_data_dir.path()) : files_location;

        if (!fs::exists(files_location))
            return;

        for (auto file : files)
        {
            read_impl(files_location / fs::path{ file }.replace_extension(m_file_ext));
        }
    }

    void DataManager::read(std::vector<fs::path> files, fs::path files_location)
    {
        files_location = files_location.empty() ? m_data_dir.path() : files_location;
        files_location = files_location.is_relative() ? fs::canonical(files_location, m_data_dir.path()) : files_location;

        if (!fs::exists(files_location))
            return;

        for (auto file : files)
        {
            read_impl(files_location / file.replace_extension(m_file_ext));
        }
    }

    void DataManager::read_dir(std::string directory, bool recursive)
    {
        read_dir(fs::directory_entry(directory), recursive);
    }

    void DataManager::read_dir(fs::directory_entry directory, bool recursive)
    {
        fs::path dir = directory.path().is_relative() ? fs::canonical(directory, m_data_dir.path().parent_path()) : directory;

        if (!fs::exists(dir))
            return;

        if (recursive)
        {
            m_tmp_data_dir = fs::canonical(directory,m_data_dir.path().parent_path());
            read_recursive(dir);
            m_tmp_data_dir = fs::path{};
        }
        else
        {
            read_non_recursive(dir);
        }
    }

    void DataManager::read_impl(fs::path&& file_name)
    {
        json tmp;
        std::ifstream in_file(file_name, std::ifstream::in);
        in_file >> tmp;
        in_file.close();

        file_name.replace_extension("");
        if (!m_tmp_data_dir.empty())
        {
            file_name = lexically_relative(file_name, m_tmp_data_dir);
        }
        else
        {
            file_name = file_name.filename();
        }

        std::string input_file = file_name.generic_string();
        if (input_file.front() == '\\' || input_file.front() == '/')
    {
        input_file = input_file.substr(1);
    }

        for (json::iterator it = tmp.begin(); it != tmp.end(); ++it)
        {
            add(it.key(), it.value(), input_file);
        }

        m_is_empty = false;
    }

    void DataManager::read_non_recursive(fs::path dir)
    {
        fs::directory_iterator dir_it{ dir };

        for (auto& item : dir_it)
        {
            fs::path tmp = item.path();
            if (tmp.extension() == m_file_ext)
            {
                read(tmp.filename().generic_string(), tmp.parent_path());
            }
        }
    }

    void DataManager::read_recursive(fs::path dir)
    {
        fs::recursive_directory_iterator rdir_it{ dir };
        for (auto& item : rdir_it)
        {
            fs::path tmp = item.path();
            if (tmp.extension() == m_file_ext)
            {
                read(tmp.filename().generic_string(), tmp.parent_path());
            }
        }
    }

    void DataManager::write_impl(fs::path file, std::ofstream & out_file) const
    {
        auto data = m_data.at(lexically_relative(file, m_tmp_data_dir).generic_string());

        out_file.open(file.replace_extension(m_file_ext), std::ofstream::out);
        out_file << std::setw(4);
        out_file << data;
        out_file.close();
    }

    void DataManager::write(fs::path data_dir) const
    {
        fs::path dir = data_dir.empty() ? m_data_dir.path() : data_dir;
        dir = dir.is_relative() ? fs::canonical(dir, m_data_dir.path().parent_path()) : dir;
        m_tmp_data_dir = dir;

        if (!fs::exists(dir))
        {
            fs::create_directories(dir);
        }

        std::ofstream file;
        for (auto item : m_data)
        {
            fs::path target = dir / fs::path(item.first);
            if (!fs::exists(target.parent_path()))
            {
                fs::create_directories(target.parent_path());
            }

            write_impl(target, file);
        }
        m_tmp_data_dir = fs::path{};
    }

    std::ostream & operator<<(std::ostream & out, DataManager const & dm)
    {
        for (auto item : dm.m_data)
        {
            out << item.first << ":\n";
            out << item.second << "\n";
        }
        return out;
    }
}

Example of usage:

#include "DataManager.hpp"

int main()
{
    utils::json test_json = R"({
        "id":0,
        "val" : {
            "obj1" : {
                "name":"obj1",
                "nums" : [1,2,3]
            },
            "obj2" : {
                "name":"obj2",
                "nums" : [4,5,6]
            }
        }
    })"_json;

    std::string test_str = R"({
        "id":0,
        "val" : {
            "obj1" : {
                "name":"obj1",
                "nums" : [1,2,3]
            },
            "obj2" : {
                "name":"obj2",
                "nums" : [4,5,6]
            }
        }
    })";

    utils::DataManager dm("../my_data");
    dm.add("test_obj1", test_json);
    dm.add("test_obj2", test_str, "subdir/test_file");
    dm.write();

    return 0;
}

The code should create this:

-some_dir
    -/working_dir
        -program.exe
    -/my_data
        -/subdir
            -test_file.json
        -settings.json

Edit:

I used this json library: https://github.com/nlohmann/json

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.