Schema

Since VAX is not bounded to any specific domain language — you create your own!

Table of contents:

Schema skeleton

An advanced schema skeleton has following sections:

colors:
    # Color aliases for future referencing
types:
    # Define types, their inheritance and type parameters
groups:
    # Define groups of components
components:
    # Define your components and their structure
dictionaries:
    # Define key-value dictionaries for using in value pickers

No section is required, but obviously you'll want to define two main sections, which are types and components.

Colors

Here you define a list of color aliases that can be referenced throughout the remaining schema:

colors:
  relation: "#cf3"
  table: "#0ff"
  coreNode: "0-#f80-#da0:50-#520"
  tableNode: "0-#0ff-#0dd:20-#111"

types:
  FromClause:
    color: @relation # Will be substitued with "#cf3"
    title: FROM

components:
  Select:
    group: core
    title: Запрос (SELECT)
    color: @coreNode # Here we use a color alias again
    # ...

Color formats are enlisted at RaphaelJS docs (bottom section of Element.attr()).

Types

Types in VAX are somewhat similar to the types that inhabit statically-typed programming languages, like Java/C++/Scala. Especially Scala :)

Where do you use types? Nodes in VAX have input and output sockets. The type of an output socket must conform to the corresponding type of the input socket. Types help to build valid blueprints, which are easier to validate and interpert. They also guide end users when they're blueprinting.

A VAX type can either extend others or be parameterized:

types:
  # Any is a supertype
  Expr: # Expr extends Any
    color: "#fff" # The wires of this type will be colored as "#fff"
    title: Expression # Provides a meaningful title, that'll be displayed on a blueprint
  Integral:
    color: @num # Use color aliases for your convenience
    extends: Expr # Integral extends Expr and Any
  Numeric:
    extends: Integral # Numeric extends Integral => Numeric extends Expr, and Numeric also extends Any
    color: @num
  String:
    extends: [Expr]
    color: @str
    title: Text
  NumericString:
    extends: [Numeric, String] # Here we define multi-inheritance
    color: @str
    title: Text with a number
  List:
    typeParams: [A] # This is like a type constructor, so we'll have to specify 'A' with a real type, e.g. List[Numeric]
    color: @arr
  Map:
    typeParams: [A,B] # Yep, we can have multiple type parameters, e.g. Map[String,Numeric], but I doubt that you'll need one :)

Apparently, you extend types from others and define paremetrized types. Also don't forget to set colors and titles.

Let's see how components use types:

components:
    ToString: # def ToString(I:Any):String = I.toString
        in:
            I: Any # Just a usual type
        out:
            O: String

    Repeat: # def Repeat[T](I: T):T = I
        typeParams: [T] # The editor will ask you to specify the type parameter 'T'
        in:
            I: @T # Uses the specified parameterised type
        out:
            O: @T

    Plus: # def Plus[T <: Expr](A: T, B: T): T = A + B
        typeParams: [T]
        typeBounds: {T: {<: Expr}} # Type parameter 'T' is upper bounded by type 'Expr'
                                   # which means we have to provide a type that inherits from 'Expr'
        in:
            A: @T
            B: @T
        out:
            O: @T

    ListLength: # def ListLength(L: List[Any]):Numeric = L.size
        in:
            L: List[Any] # Don't forget about the supertype Any
        out:
            O: Numeric

    ZipWithIndeces: # def ZipWithIndices[T](A: List[T]):Map[Numeric,T] = ...
        typeParams: [T]
        in:
            A: List[@T]
        out:
            M: Map[Numeric,@T] # Crazy stuff!

While you are defining components you reference types within input and output sockets. You can also have type parameters on a particular component and the use it by alias, e.g. @T.

The weird syntax typeBounds: {T: {<: Expr}} in terms of YAML means:

typeBounds:
    T:
        "<": Expr

and defines an upper type bound from 'Expr' for type parameter 'T'.

When a user wants to create a node from a component that has type parameters, the editor will ask to specify them, and then will substitute the type aliases with the real types. E.g. for a component:

IfThenElse:
    typeParams: [T]
    in:
        Condition: Boolean
        onTrue: @T
        onFalse: @T
    out:
        O: @T

We get:

Groups

When the number of components increases, it gets hard to navigate through them. You can put components into related groups, which are then displayed in component selector:

groups:
  core: Core elements
  ops: Basic calculations
  bool: Logic operations
  str: Working with strings

components:
    Result:
        group: core # The component 'Result' is now in the group 'core'
        in:
          I: Any

    ToString:
        group: str # Putting 'ToString' into to the string operations group, 'str' :)
        in:
          I: Any
        out:
          O: String

If you don't specify the group of a component, it gets placed into the group 'Uncategorized', which is one of the system groups:

return _.defaults(schema.groups, {
  '_default':       'Uncategorized',
  '_userFunctions': 'User functions',
  '_ufElements':    'User functions elements'
});

It's recommended that you don't use leading underscore in your group names.

Components

Components are building blocks of your language or domain. In conjunction with the types, you can create a very rich language. You can use type system constraints to be sure you get valid blueprints. End users can go even further and organise basic components into functions and reuse them later, thus enriching the design language.

So a component is a template for a real blueprint node. The template contains: input and output sockets definitions, attributes definitions, type parameters with type bounds and of course stuff like title, color and group. See the editor section for a visual reference.

A minimal component definition doesn't really require anything:

components:
    Useless: # A unique name, the one you'll see within serialized tree

In order to be useful a node needs at least one input or output socket definition:

components:
    ToString:
        in: # input socket section
          I: Any # We define an input with a name 'I' that accepts a value of type 'Any'
        out: # output socket section
          O: String # We define an ouput with name 'O' that produces a value of type 'String'

Socket names should be unique within their respective section.

You can also give titles to your sockets:

components:
    Plus:
        in:
          L:
            title: Left
            type: Expr
          R:
            title: Right
            type: Expr
        out:
           R:
            title: Result
            type: Expr

To make your component more user-friendly, you can give it a title, a color and a group:

components:
    Result:
        title: Resulting value    # You'll see that in the editor
        color: @coreNode          # The color of your node, color aliases are allowed
        group: core               # The name of the group, which contains this component
        in:
          I: Any

Sometimes you need to provide a node with an attribute that can be set by a user on the blueprint. Minimal attribute definitions consist of a name and a type:

components:
    NumberLiteral:
        attrs: # attributes section
            V: Numeric # Unique attribute name within component
        out:
          O: Numeric

It may seem that types don't make sense for attributes, and it's true. In fact they don't affect anything at all, for now. They're reserved for future use, like "socketable" attributes and the ability for users to define attributes in the user functions. A good rule of thumb is to use closest meaningful type or just resort to Any.

As you might expected attributes can be configured with a title and a default value:

components:
    NumberLiteral:
        attrs:
            V:
                type: Numeric
                title: Value
                default: 1
        out:
          O: Numeric

By default, for modifying an attribute's value, the editor uses just a simple <input> element. But you can define your own value pickers. E.g. there's a built-in value picker called dictionary, which requires a defined dictionary in your schema:

components:
    TrigonometricFn:
        title: Trigonometic function
        in:
            I: Numeric
        attrs:
            Fn:
                type: Any
                title: Function
                default: sin
                valuePicker:
                    type: dictionary
                    dictionary: TrigonometricFunctions
        out:
          O: Numeric

dictionaries:
    TrigonometricFunctions: # Referenceable unique name of a dictionary
        title: Trigonometic functions of a single argument
        values: # Name -> Title
            sin: sin(X)
            cos: cos(X)
            tg:  tan(X)
            ctg: cot(X)

In order to register your own value picker you have to resort to the registerValuePicker(type, valuePicker) function on a VAX object, e.g.:

// we have to provide an object with two functions: 'getValueTitle' and 'invoke'
var promptValuePicker = {
    getValueTitle: function(value, options) // used to map your plain value to a title, options are passed from your schema
    {
        return value.toString();
    },

    invoke: function(value, callback, options) // 'value' holds a current state of an attribute, 'options' are passed from schema
    {
        val newValue = prompt(options.promptMessage, value);
        callback(newValue); // don't forget to trigger callback with a new value from your picker
    }
}

var myVAX = new VAX('containerId', {schema: /*...*/}); // create your VAX object
myVAX.registerValuePicker('prompt', promptValuePicker); // 'prompt' will be our picker name

With this value picker defined, we can use it in our schema:

components:
    PromptNumber:
        attrs:
            V:
                type: Numeric
                title: Value
                default: 1
                valuePicker: # this is what is passed as 'options'
                    type: prompt # picker name
                    promptMessage: "Provide a number, please ... "
        out:
          O: Numeric

Attributes may be substituted with input sockets. Also future versions of the editor will provide input sockets that can be set with default values just like attributes.