Kubik (pronounced [ku:bik]) is a domain-specific language define validation rules over configuration graphs. Kubik uses JavaScript-like syntax that allows any arbitrary rules to be easily be written and read.
Configurations that have tree or graph structure are not easy to navigate and validate for correctness. Some examples are cloud environments, networking, Kubernetes, etc.
Kubik enables traversal and validation of nodes using a combination of declarative and imperative rules.
Kubik rule consists of two parts: target and rule scripts. Target script declares on which nodes should the validation be executed. Rule script validates each node that matches the target script.
Lets consider following example. We have a graph representation of a Car that has front and rear Doors, each of those have a Window.
Lets ensure that all doors are locked, before we can drive the car.
Target:
select('Door')
Rule:
if (!item.props.locked) {
error()
}
We want to make sure that only the rear window is closed.
Target:
select('Door')
.name('rear')
.child('Window')
Rule:
if (!item.props.closed) {
error()
}
Each node in the graph has a type and name. In the example above the type can be "Door" and "Window" and the names are "front" and "rear". Nodes can also be marked using value labels.
Multiple sets of key-value or array objects can also be associated with each node.
The purpose of the target script is to select a subset of nodes that matches required criteria. The selected node are be passed along to the rule script for validation.
The target script starts with select statement that takes the node type as an input. That statement selects all nodes of the given type.
select('Door')
Nodes can be traversed further by going down one level using child.
select('Door')
.child('Window')
For further capabilities, lets consider bit more complex example below. The graph represents a fleet of trucks that move (shipping) containers, materials and corresponding drivers.
Nodes can be filtered by name:
select('Material')
.name('Corn')
Or by matching myltiple names. The target script below selects both "Corn" and "Horse" materials:
select('Material')
.name('Corn')
.name('Horse')
Arbitrary programmable filters can be defied and expressed using JavaScript syntax. To filter "Maersk" containers:
select('Container')
.filter(({item}) => {
return item.name.startsWith('Maersk');
}))
Selecting children of filtered objects. Script below targets drivers of 3 or less axle trucks.
select('Truck')
.filter(({item}) => {
return (item.props.axleCount <= 3);
}))
.child("Driver")
Descendents can also be selected using a similar method. The resulting nodes are "Corn" and "Waste" matrials.
select('Truck')
.filter(({item}) => {
return (item.props.axleCount > 3);
}))
.descendant("Material")
Multiple filters, child and descendent queries can be chained together.
select('Truck')
.filter(({item}) => {
return (item.props.axleCount >= 5);
}))
.child("Container")
.filter(({item}) => {
return (item.props.weight >= 4);
}))
.descendant("Material")
.filter(({item}) => {
return item.props.biohazard;
}))
Nodes selected from the target script are be passed to rule script for evaluation. Rules are expressed using JavaScript syntax. The rule script has access to the node, along with the name, properties and also has access to the entire graph.
The current node is represented in item variable. Just like in case of target script, in rule script properties are accessed through props field. A call to error() function marks the node as invalid.
Some examples of rules below. Validating that hazardous materials are in containers below 4 tons:
if (item.props.biohazard && item.parent.props.weight > 4) {
error();
}
Enforcing California drivers not to transport livestock:
if (item.props.state == 'CA'))
{
for(var container of item.parent.children('Container'))
{
for(var material of container.children('Material'))
{
if (material.props.livestock)
{
error();
}
}
}
}