Variable Library (VL)

VL is a C++ library that provides a way to configure data structures for applications from primitive types to container types Object and List which in turn can include any type, so you can get a tree data structure with any level of complexity. The library support conversion to and from JSON.

Intention

The main purpose of VL is to free your hands from writing data structures using a programming language. It is not needed to write classes like Animal or DeliveryBox with properties like amount, size or color using a high-level programming language. You can achieve even inheritance using just a properly constructed data structure.

Dev process improvements

Speed up

VL provides all necessary features to significantly speed up the development process working with data models. When you are free from writing class by class representing data models which can be changed time to time it becomes much easier to work with such codebase not only because of less amount of work, but also because the code becomes cleaner, less cluttered, easier to read.

Cheapening

As no longer high-level programming language is needed for data model creation it becomes possible to delegate the data model designing process to a content manager, game designer, data model designer, e.t.c.

Features

Supported data types

Primitive types

Containers

Boolean

Number

String

Null

Object

List

Primitive types

Type

Value

Description

Boolean

true, false

Standard boolean data type, holds bool type as a data

Number

2.22507e-308 - 1.79769e+308

0,1, 2, …, 0.4, 3.5, e.t.c.

Stored as double C++ type. There is no separation between Integer and Float. You can interpret it as Integer if you want in your application as you expect that type in a particular field.

String

Any set of single-byte characters supported by C++

Single-byte character set. Uses std::string as a data in the storage.

Null

No data

A variable whose operator bool() always returns false. Plus to that the basic class has a method IsNull() to check if an abstract variable is a Null variable.

Containers

Container can include:

Two types of containers are supported: Object and List. Both of them conain a data structure for its child nodes that is wrapped into std::shared_ptr to prevent copying of the data when assigning one container variable to another. To make a deep copy of a container with it’s data there is method Copy().

Object

Object is a tree data structure which can hold any other type including itself under a string-typed key. Uses std::unordered_map C++ type as a container for all of the child nodes and std::string for keys that is wrapped into std::shared_ptr.

List

List represents the data structure of the same name containing elements in ordered manner. Uses std::vector C++ type for storing its child elements that is wrapped into std::shared_ptr..

Example

In this example Objects are presented as green circles and Lists are blue. Connections of objects with its child nodes contain a text string which represents a key of a child node.

Prototyping

VL provides more flexibility and extensibility through the prototyping mechanism. It allows you reuse properties of existing objects saving your time, efforts and the data structure space. An Object can inherit another’s object properties storing a reference to it in it’s “proto” property. A prototype object can in turn have it’s own prototype that with the ascending proto creates a prototype chain. It allows VL to find a requested property through the chain of prototypes if it doesn’t exist among the requested object’s fields.

Supported features

Using prototyping in VL you can:

Example

Suppose we have an object A with properties “title”, “weight” and “color”, and we have an object B that inherits all properties of A, overrides “title” and “weight” and introduces a new property “colored”. With VL such model can be converted to a such JSON:

{

"A": {

    "title": "Object of class A",

    "weight": 3,

    "color": "white"

},

"B": {

    "proto": "A",

    "title": "Object of class B",

    "weight": 45,

    "colored": false

}

}

Here we declare “proto” property of object B which contains name of the inherited object. When such model is loaded using JSONLoader the object B will have a smart pointer to the object A in its “proto” field.

You can even use objects in lists as prototypes. For example:

{

        "orange": {

            "proto": "fruitList.[0]",

            "title": "Orange"

        },

        "apple": {

            "proto": "fruitList.[0]",

            "title": "Apple"

        },

        "fruitList": [

            {

                "title": "Fruit"

            },

            {

                "proto": "fruitList.[0]",

                "title": "Orange"

            },

            {

                "proto": "fruitList.[0]",

                "title": "Apple"

            }

        ]

}

Here fruitList is a List which contains all fruit including the one with title “Fruit” which is used as a prototype for all other objects. “Orange” and “Apple” are objects stored in both a fruitlist as elements and in a root node with keys “orange” and “apple”. It doesn’t matter how the fruitList will be changed, it won’t affect the proto reference as it is stored in memory just as a C++ shared pointer to the “Fruit” object and what you see in the JSON is just a single-moment text representation of the model from the memory, so even if the Fruit shifts to a second position in the list it will be referenced in the all “proto” fields as fruitList.[1]”.

Performance and memory

Time complexity

Object

List

Access

O(1)

O(1)

Addition

O(1)

O(1) back, O(n) in the middle

Removal

O(1)

O(1) back, O(n) from the middle

Extensibility

Usage

Create a variable

auto s = vl::String("Hello");

std::cout << s.Val() << "\n";

 

auto n = vl::Number(321.4);

std::cout << n.Val() << "\n";

 

auto b = vl::Bool(true);

std::cout << (b.Val() ? "true" : "false") << "\n";

Output:

Hello

321.4

true

Create a container

Object

You can put any variable to an object. There is a handy method ForeachProp(pred) for object’s properties iteration. To print the value of a variable the method ToStr() is used

// Create an object with some fields

auto pear = vl::Object();

pear.Set("isFruit", true);

pear.Set("color", "Yellow");

pear.Set("radius", 0.3f);

pear.Set("branchCount", 1);

// Print the content

std::cout << "Object content:\n";

pear.ForeachProp([&](auto& k, auto& v) {

        std::cout << "        [" << k << "]: " << v.ToStr() << "\n";

        return true;

});

Output:

Object content:

        [isFruit]: true

        [color]: Yellow

        [radius]: 0.3

        [branchCount]: 1

List

The same with Lists. Any variable can be put to any container.

// Create a list with differently-typed elements

auto list = vl::List();

list.Add(pear);

list.Add(vl::String("A text"));

list.Add(vl::Bool(true));

list.Add(vl::Number(41));

// Print the content

std::cout << "\nList content:\n";

for (int i = 0; i < list.Size(); i++)

        std::cout << "        [" << i << "]: " << list.At(i).ToStr() << "\n";

 

Output:

List content:

        [0]: {}

        [1]: A text

        [2]: true

        [3]: 41

Set a prototype

auto fruit = vl::Object();

fruit.Set("isFruit", true);

fruit.Set("title", "Fruit");

fruit.Set("color", "Green");

fruit.Set("radius", 0.2f);

fruit.Set("branchCount", 0);

 

auto pear = vl::Object();

pear.SetPrototype(fruit);

pear.Set("title", "Pear");

pear.Set("color", "Yellow");

pear.Set("radius", 0.3f);

pear.Set("branchCount", 1);

pear.Set("isSweet", true);

 

// Print the content

std::cout << pear.Get("title").AsString().Val() << " properties:\n";

pear.ForeachProp([&](auto& k, auto& v) {

        std::cout << "        [" << k << "]: " << v.ToStr() << "\n";

        return true;

}, true);

Output:

Pear properties:

        [title]: Pear

        [proto]: {}

        [color]: Yellow

        [radius]: 0.3

        [branchCount]: 1

        [isSweet]: true

        [isFruit]: true

As you can see the result object contain properties of both Pear and Fruit. Retrieving a particular property using Get(propId) method returns the last overload.

JS-like behaviour

VL is designed such a way that any container is always shared when assigned and any primitive type is copied by default. To deep-copy a container there is a method Copy() for that. It is similar to JS: when you create a variable of an object or a list and assign it to another variable and then change something in it - it will be affected both variables.

For example the code below:

auto vl::Object o;

o.Set(“title”, “Fruit”);

auto a = o;

a.Set(“title”, “Vegetable”);

std::cout << o.Get(“title”).AsString().Val() << “\n”;

Will print:

> Vegetable

JSON Converter

Using JSONConverter you can write and read JSON files from and to an Object.

Writing

Here we create a little bit complicated data structure and store it to a file “write_test.json”. After that we use method JSONConverter::JSONStr to just print the model without storing it.

auto pear = vl::Object();

pear.Set("isFruit", true);

pear.Set("color", "Yellow");

pear.Set("radius", 0.3f);

pear.Set("branchCount", 1);

auto bush = vl::Object();

bush.Set("leafColor", "Green");

bush.Set("isTree", true);

bush.Set("leafPerBranch", 6);

bush.Set("branches", vl::List());

bush.Set("x", 0);

bush.Set("y", 0);

auto branch = vl::Object();

branch.Set("leafCount", 10);

branch.Set("fruit", vl::Object());

branch.Set("branches", vl::List());

auto branch1 = branch.Copy()->AsObject();

branch1.Set("leafCount", 9);

auto branch2 = branch.Copy()->AsObject();

branch2.Set("leafCount", 3);

branch2.Set("fruit", pear);

branch1.Get("branches").AsList().Add(branch2);

bush.Get("branches").AsList().Add(branch1);

vl::JSONConverter converter;

const char* fName = "write_test.json";

if (converter.Store(bush, TypeResolver(), fName, {true}))

{

        std::cout << "Stored to '" << fName << "':\n";

        std::cout << converter.JSONStr(bush, TypeResolver(), { true });

}

else

        std::cout << "Store error\n";

Output:

Stored to 'write_test.json':

{

    "leafColor": "Green",

    "isTree": true,

    "y": 0,

    "leafPerBranch": 6,

    "branches": [

        {

            "leafCount": 10,

            "fruit": {},

            "branches": [

                {

                    "leafCount": 10,

                    "fruit": {},

                    "branches": []

                }

            ]

        }

    ],

    "x": 0

}

Reading

Let’s load the json generated in the previous example.

auto converter = vl::JSONConverter();

vl::Object object;

const char* fName = "write_test.json";

if (converter.Load(object, fName))

        std::cout << "Loaded from '" << fName << "':\n";

std::cout << converter.JSONStr(object, TypeResolver(), { true });

Output:

Loaded from 'write_test.json':

{

    "leafColor": "Green",

    "isTree": true,

    "leafPerBranch": 6,

    "y": 0,

    "branches": [

        {

            "leafCount": 10,

            "branches": [

                {

                    "leafCount": 10,

                    "branches": [],

                    "fruit": {}

                }

            ],

            "fruit": {}

        }

    ],

    "x": 0

}

Limitations

As VL doesn’t store links to parents in it’s core implementation, you cannot get a human-readable identifier of a prototype without specifying TypeResolver to JSONConverter methods, but it is implemented in the DMBCore library that provides some utility for working with typed objects and has its own TypeResolver based on VLBackwardTraversable extension, so you can store and load JSONS like these:

{

    "New object": {

        "count": 3

    },

    "Concrete_New object": {

        "proto": "New object"

    }

}

or

{

"fruitList": [

    {

        "title": "Fruit"

    },

    {

        "proto": "fruitList.[0]",

        "title": "Orange"

    }

]

}

Projects using VL

DMBCore

DMBCore is a library for working with external data models (creating, storing, loading from JSON) built on VL and can be considered as its extension, but because of very specific purposes it is a separated project.

QVL

QVL is a Qt plugin used to serve Qt data models (including ListModel and QObject) using VL as its data layer for Qt models. It is also widely used in DMBCore library that is also used in QVL.

DataModelBuilder

DataModelBuilder is a graphical application based on Qt in conjunction with QVL plugin which serves as a GUI client for database and data model design. All VL usage for DataModelBuilder is encapsulated into QVL library and there is no need to use C++, but only for few functions written especially for this application for working with URLs.

SkillBuilder

SkillBuilder is an example of end application that uses QVL for working with data models and has no C++ code, but has a rich functional implemented for its purposes using Qt’s QML in conjunction with QVL.