C++

You defined a data model in DSM and ran Kibo over it with the kibo-template-viper pack. For C++, what you get back is not a wrapper library: it is C++17 value types — structs, enum classes, key classes, STL containers — and the generated code that crosses them to the Viper C++ runtime. You write ordinary, value-semantic C++; the codec and the attachments cross to the runtime’s dynamic Viper::Value for you.

The generated C++ is the pack’s base reference: the Python and TypeScript packages offer what it offers, with its restrictions. Producing it is kibo-project’s job, and which files a project renders is the feature selection described in Templated Features. The runtime it links against is Viper C++ (commercial) on its 1.2 line, with its static layer (Viper_StaticType, Viper_StaticWriter, Viper_StaticReader, Viper_StaticHash).

The examples use the features model of the generator’s laboratory, devkit-codegen-test, whose infrastructure name (Kibo’s -n) is features. It declares one namespace, Demo:

namespace Demo {7fdd9ba9-b85e-4b33-a749-9b9615e43452} {

concept ConceptA;
concept ConceptB;
concept ConceptC is a ConceptB;
concept ConceptD;

club Klub;
membership Klub ConceptC;
membership Klub ConceptD;

enum EnumerationE {
  a,
  b,
  c
};

struct StructureS {
  float f_float;
  string f_string;
};

struct StructureT {
  string field_string;
  StructureS field_structure_s;
};

// One field of every DSM type: numbers, strings, blobs, keys and containers.
struct StructureU {
  uint8 f_uint8;
  int64 f_int64;
  vector<uint8> f_vector;
  // …
};

// The same fields, each with a default value.
struct StructureV {
  uint8 f_uint8 = 8;
  string f_string = "Forty Two";
  // …
};

attachment<ConceptA, StructureV> properties;
attachment<ConceptB, StructureT> propertiesB;
attachment<Klub, StructureV> propertiesD;
// …
};

What is generated

A DSM namespace is a unit: a file-name prefix and a C++ namespace inside the infrastructure’s. Demo in the infrastructure features gives features_demo_data.hpp and namespace features::demo, so two namespaces of one model may declare the same name. Which files are rendered depends on the features a project selects:

Feature

Files

What they hold

Base

<ns>_<unit>_data, _model, _codec, <ns>_codec, <ns>_any_concept, <ns>_resources

structures, enumerations, keys; their identity in the definitions; the codec to a Viper::Value

Fields

<ns>_<unit>_fields

every field’s name (fields::S::f), for code that goes through the dynamic API

Paths

<ns>_<unit>_paths

every field’s path (paths::S::f()); requires Fields

DatabaseAttachments

<ns>_<unit>_database_attachments

one scope per attachment, its operations over a Viper::Database

CommitAttachments

<ns>_<unit>_commit_attachments

one scope per attachment, its operations over a Commit state; requires Paths

Reads

<ns>_<unit>_reads

one scope per attachment, reading only, over a Viper::AttachmentGetting

Pool

<ns>_<pool>_pool

the functions an application implements, and the pool it exposes

PoolRemote

<ns>_<pool>_remote

the same pool, called through a service

AttachmentPool

<ns>_<pool>_attachment_pool

an attachment function pool, as Pool; requires CommitAttachments

AttachmentPoolRemote

<ns>_<pool>_attachment_remote

the same, called through a service; requires CommitAttachments

Every file starts with a header naming the generator, the templates and the runtime, and says Do not edit by hand: it is a build artifact, regenerated on every model change. A defect in it is fixed in the DSM model or in the templates, never in the output.

The examples below include:

#include "features_codec.hpp"              // the codec, for every namespace of the model
#include "features_demo_database_attachments.hpp"
#include "features_demo_commit_attachments.hpp"
#include "features_demo_reads.hpp"              // with the Reads feature
#include "Viper_CommitDatabase.hpp"
#include "Viper_CommitMutableState.hpp"
#include "Viper_CommitState.hpp"
#include "Viper_CommitStateBuilder.hpp"
#include "Viper_CommitStore.hpp"
#include "Viper_Database.hpp"

using namespace features;

Structures and enumerations

A structure is a struct with public fields, a default constructor and a constructor taking every field in declaration order; a one-field structure converts from its field. A new structure holds the model’s defaults — the default a field declares, or else the default of its type:

demo::StructureS s(1.5f, "hello");           // every field, in declaration order
demo::StructureS empty;                      // {0.0, ""}
s.f_string = "world";

demo::StructureV v;
v.f_uint8;                                   // 8, declared in the model
v.f_set;                                     // std::set<std::uint8_t>{1, 2, 3}

Copying copies. Equality and order are generated (==, !=, <), and so is a std::hash specialization, over the runtime’s static hash:

bool const same{s == demo::StructureS{1.5f, "world"}};
bool const before{empty < s};
std::size_t const h{std::hash<demo::StructureS>{}(s)};

An enumeration is an enum class whose enumerators are the DSM cases, capitalized: demo::EnumerationE::A, B, C.

The types map to C++ as you would write them: std::string, the fixed-width integers, std::vector, std::set, std::map, std::optional, std::tuple, std::variant, std::array for a vec and an array of columns for a mat, Viper::XArray for an xarray, Viper::Any for an any, Viper::UUId, Viper::Blob.

The codec

A value crosses to a Viper::Value through the codec, which copies, and back:

auto const value{codec::encode(s)};                          // a Viper::ValueStructure
auto const back{codec::decode<demo::StructureS>(value)};     // equal to s
auto const definitions{codec::definitions()};                // the model, as the runtime knows it

encode takes any generated type and any container of them; decode<T> checks the value’s type and throws a Viper::Error when it is not T. JSON, XML and a hexdigest are one runtime call on what encode returns: the pack generates the bridge, not what composes it with a runtime feature. The binary form is the runtime’s static layer — write(w, v) on a Viper::StaticWriter::Writer, read(r, tag<T>{}) on a Viper::StaticReader::Reader, found by argument-dependent lookup.

Keys

A concept or a club gives a final key class: an instance id and the runtime id of the instance’s concept. create() mints a fresh instance, and the default constructor gives the invalid key:

auto const a{demo::ConceptAKey::create()};
demo::ConceptAKey const invalid;
a.isValid();                                 // true
invalid.isValid();                           // false
a.instanceId();                              // a Viper::UUId

Keys are values — ==, <, std::hash — so they go into std::set, std::map and std::unordered_set as they are.

A child key widens to its parent’s by an implicit conversion or toParentKey(), and a parent key narrows with Child::from(anyConceptKey), or parent.asChildKey() for a child declared in the parent’s namespace, which the parent can name. A club key converts from a member’s key and narrows to each member the same way. Every key gives toAny(), also named toAnyConceptKey():

auto const c{demo::ConceptCKey::create()};
demo::ConceptBKey const b{c};                                 // widening, implicit
demo::ConceptBKey const parent{c.toParentKey()};              // widening, named
std::optional<demo::ConceptCKey> const child{b.asConceptCKey()};          // narrowing
std::optional<demo::ConceptCKey> const fromAny{demo::ConceptCKey::from(b.toAny())};

demo::KlubKey const klub{c};                                  // a club key, from a member's
std::optional<demo::ConceptDKey> const notD{klub.asConceptDKey()};       // std::nullopt

AnyConceptKey const anyone{c.toAnyConceptKey()};
bool const sameInstance{anyone == b.toAny()};                 // true

A narrowing answers std::nullopt when the instance is not one of the target concept.

Attachments

Viper has two databases that share nothing, and an attachment is generated once per usage, each a scope <ns>::<unit>::<usage>::<Concept>::<attachment> with only that usage’s operations:

Usage

Over

Operations

database_attachments

a Viper::Database

keys, has, get; set, del inside a transaction

commit_attachments

a Commit state

keys, has, get over a Viper::AttachmentGetting; set, diff and the field operations over a Viper::CommitAttachmentMutating

reads

a Viper::AttachmentGetting, a Viper::Database or a Commit state

keys, has, get

runtimeId is the attachment’s runtime id, a constant, and descriptor() the runtime’s Viper::Attachment; both are the attachment’s, whatever the usage.

On a Viper::Database, extended with the model once:

namespace stored = demo::database_attachments::ConceptA::properties;

auto const db{Viper::Database::createInMemory()};
db->extendDefinitions(codec::definitions());
auto const key{demo::ConceptAKey::create()};

db->beginTransaction(Viper::DatabaseTransactionMode::Deferred);
stored::set(db, key, demo::StructureV{});
db->commit();

std::optional<demo::StructureV> const document{stored::get(db, key)};
std::set<demo::ConceptAKey> const held{stored::keys(db)};

db->beginTransaction(Viper::DatabaseTransactionMode::Deferred);
stored::del(db, key);
db->commit();

On a Viper::CommitDatabase, through commit_attachments, the writes go to a Viper::CommitMutableState — built from CommitStateBuilder::initialState(db) for the first commit, CommitStateBuilder::state(db, commitId) to carry on — and are stored once it is committed:

namespace properties = demo::commit_attachments::ConceptA::properties;

auto const cdb{Viper::CommitDatabase::createInMemory()};
cdb->extendDefinitions(codec::definitions());

auto state{Viper::CommitMutableState::make(Viper::CommitStateBuilder::initialState(cdb))};
properties::set(state, key, demo::StructureV{});
properties::setF_string(state, key, "v1");                   // a field operation
properties::unionF_set(state, key, {7});
auto const v1{cdb->commitMutations("First version", state)};

state = Viper::CommitMutableState::make(Viper::CommitStateBuilder::state(cdb, v1));
properties::setF_string(state, key, "v2");
auto const v2{cdb->commitMutations("Second version", state)};

auto const past{Viper::CommitStateBuilder::state(cdb, v1)};  // every commit stays readable
std::string const first{properties::get(past, key)->f_string};   // "v1"

The field operations are named after the field: set<Field> for every field, and for a collection field union<Field>, subtract<Field> (set, map), update<Field> (map), insert<Field>, remove<Field> (xarray). A field operation on a key that holds no document does nothing: set the document first. A Viper::CommitStore keeps that thread for an application, with undo and redo:

auto const store{Viper::CommitStore::make()};
store->use(cdb);
store->dispatch("Third version", [&](std::shared_ptr<Viper::CommitAttachmentMutating> const & mutating) {
    properties::setF_string(mutating, key, "v3");
});
store->undo();

The commit model itself — heads, history, merges — is the Commit subsystem’s.

Code that reads documents without knowing which database holds them — a library that applications of both kinds compile — reads through reads, over a Viper::AttachmentGetting:

std::string firstString(std::shared_ptr<Viper::AttachmentGetting> const & getting,
                        demo::ConceptAKey const & key) {
    auto const document{demo::reads::ConceptA::properties::get(getting, key)};
    return document ? document->f_string : std::string{};
}

firstString(db, key);                                         // a Viper::Database
firstString(Viper::CommitStateBuilder::state(cdb, v2), key);  // a Commit state

A library does not choose a usage for the applications that use it: it reads through reads, which each application asks for beside its own usage, in its own package. The laboratory’s consumers site renders the three shapes side by side — a Commit application, a Database application, and a library both compile — and checks that each gets its usage and nothing of the other.

Pools

A function pool is a namespace, <ns>::<pool>. The laboratory’s service model (the infrastructure service) declares Tools:

function_pool Tools {7aa5aea2-c9de-4f91-8371-7995aca8c947} {
    int64 add(int64 a, int64 b);
    Vector3 addVector(Vector3 a, Vector3 b);
    // …
};

With the Pool feature, the application implements the functions in that namespace, and pool() returns the Viper::FunctionPool it exposes; poolName and poolId name it:

#include "service_tools_pool.hpp"

namespace service::tools {

std::int64_t add(std::int64_t a, std::int64_t b) {
    return a + b;
}

demo::Vector3 addVector(demo::Vector3 const & a, demo::Vector3 const & b) {
    return {a.x + b.x, a.y + b.y, a.z + b.z};
}

// … every function the pool declares

} // namespace service::tools

std::shared_ptr<Viper::FunctionPool> const exposed{service::tools::pool()};

With the PoolRemote feature, a client calls the same pool through a service, with Remote over a Viper::ServiceRemote; it links without the functions only a server implements. With the AttachmentPoolRemote feature, an attachment function pool is called the same way; a function takes a CommitAttachmentMutating when it is mutable, an AttachmentGetting when it only reads:

#include "service_codec.hpp"
#include "service_tools_remote.hpp"
#include "service_player_model_attachment_remote.hpp"
#include "Viper_CommitMutableState.hpp"
#include "Viper_CommitState.hpp"

auto const remote{Viper::ServiceRemote::connect("localhost", "54328", Viper::Definitions::make())};
service::tools::Remote const tools{remote};
if (tools.isAvailable()) {
    std::int64_t const sum{tools.add(32, 10)};            // 42
}

service::player_model::Remote const players{remote};
auto const state{Viper::CommitMutableState::make(Viper::CommitState::make(service::codec::definitions()))};
auto const player{players.create(state, "zoe", service::demo::Level::Expert)};
std::optional<service::demo::PlayerKey> const found{players.hasPlayer(state, "zoe")};

Most projects do not expose their pools as a service; where services come from is the Service chapter’s subject.